diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9edd9c1..15f3989 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -309,6 +309,16 @@ jobs: with: path: artifacts merge-multiple: true + - name: Prepare shared release notes + shell: bash + env: + RELEASE_TAG: ${{ needs.version.outputs.tag }} + run: | + set -euo pipefail + node scripts/release-notes.mjs \ + --tag "$RELEASE_TAG" \ + --output "$RUNNER_TEMP/easycli-release-notes.json" \ + --body-output "$RUNNER_TEMP/easycli-release-notes.md" - name: Generate portable update manifests shell: bash env: @@ -321,7 +331,8 @@ jobs: --platform "$platform" \ --repository "${{ github.repository }}" \ --gitcode-repository "${{ vars.GITCODE_GUI_REPOSITORY }}" \ - --tag "$RELEASE_TAG" + --tag "$RELEASE_TAG" \ + --release-notes "$RUNNER_TEMP/easycli-release-notes.json" done - name: Publish release shell: bash @@ -339,7 +350,7 @@ jobs: esac prerelease="$MANUAL_PRERELEASE" - release_args=(--title "EasyCLIProxyAPI $RELEASE_TAG" --generate-notes) + release_args=(--title "EasyCLIProxyAPI $RELEASE_TAG" --notes-file "$RUNNER_TEMP/easycli-release-notes.md") if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then if [[ "$RELEASE_TAG" == *-* ]]; then prerelease=true @@ -355,6 +366,7 @@ jobs: if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then gh release upload "$RELEASE_TAG" artifacts/* --clobber + gh release edit "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/easycli-release-notes.md" else gh release create "$RELEASE_TAG" "${release_args[@]}" artifacts/* fi diff --git a/README.zh-CN.md b/README.zh-CN.md index 5561293..604a36e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -49,6 +49,8 @@ APIMart 是一个低成本的 AI 图像与视频生成 API 平台——GPT-Image 每次启动软件时,软件和内核会各自在后台自动检查一次更新;之后每进入 **版本管理** 页面 5 次(第 5、10、15 次,以此类推)自动检查一次,也可随时通过各自的 **检查更新** 按钮手动检查。页面内部刷新不重复计数,重启软件后次数归零。修改下载源不会自动检查更新,重新进入页面也会保留已保存的下载源。检查不会自动下载或安装更新。 +**版本管理** 页面同时展示当前界面语言的最新版软件更新说明,升级后仍可查看。发布者在 [`docs/release-notes/`](docs/release-notes/README.md) 中按版本分别编辑简体中文、繁体中文、英文和日文 Markdown,CI 会将内容用于 GitHub Release 正文和应用更新清单。当前语言的说明缺失时直接提示,不回退到其他语言。 + ### OAuth 账号授权 ![OAuth 账号授权](docs/screenshots/zh-CN/2.png) diff --git a/bun.lock b/bun.lock index 4c4d04e..1b5ad57 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "lucide-react": "^1.24.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", }, "devDependencies": { "@tauri-apps/cli": "^2.0.0", @@ -221,66 +222,192 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], + + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.2.0", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.43", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ=="], "browserslist": ["browserslist@4.28.6", "", { "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001803", "electron-to-chromium": "^1.5.389", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw=="], "caniuse-lite": ["caniuse-lite@1.0.30001805", "", {}, "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA=="], + "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.389", "", {}, "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg=="], "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@1.24.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA=="], + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], "node-releases": ["node-releases@2.0.51", "", {}, "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ=="], + "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], "postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], + "react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="], "react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="], + "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + "rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -289,16 +416,48 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], } } diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md new file mode 100644 index 0000000..067f6f2 --- /dev/null +++ b/docs/release-notes/README.md @@ -0,0 +1,37 @@ +# 维护更新说明 + +每个版本建立与发布 tag 同名的目录,分别维护四份 Markdown: + +```text +docs/release-notes/v0.2.98/ + zh-CN.md # 简体中文 + zh-TW.md # 繁體中文 + en.md # English + ja.md # 日本語 +``` + +发布新版本时,创建对应目录并为每种语言编写正文和分类标题。版本号和日期由页面自动展示,不需要重复写入正文。支持标题、列表、加粗、代码和 HTTP/HTTPS 链接;应用内不加载图片或原始 HTML。简体中文格式示例: + +```markdown +## 新增 + +- 描述新增的功能。 + +## 优化 + +- 描述体验上的改进。 + +## 修复 + +- 描述修复的问题。 +``` + +将说明文件随代码提交到运行发布工作流的提交中,再发布对应 tag。不要把文案写进 CI YAML。当前的 `v0.2.98/` 是本次功能的待发布说明,实际发布其他版本时请同步调整目录名。 + +发布流程读取这些文件后生成两份输出:GitHub Release 正文按语言分节展示;Windows、Linux、macOS 更新清单的 `releaseNotes` 保存语言到 Markdown 的映射,例如 `{"zh-CN":"## 新增\n…","en":"## Added\n…"}`。应用检查版本时一并获取,切换界面语言会立即切换到对应正文,无需额外请求。清单中的发布日期使用本次生成清单的时间。 + +**不做语言或内容回退。** 文件不存在或只有空白时,清单不包含该语言的正文,应用直接提示“暂未获取到当前语言的更新说明”。即使其他语言有内容,也不会代替显示。GitHub Release 对应语言一节同样显示缺失提示。不会读取旧的单文件正文、已有 Release 文本或 GitHub 自动生成内容来补齐。 + +旧清单没有按语言组织的 `releaseNotes` 时,同样显示提示,不影响版本检查和升级。已经更新到最新版也能继续查看当前语言的说明。 + +重新运行发布工作流会同步更新 Release 正文和清单;仅在 GitHub 网页修改正文不会改变已经上传的清单。说明文件的权限或读取错误会使 CI 报错,以免静默发布错误内容。 diff --git a/docs/release-notes/v0.2.98/en.md b/docs/release-notes/v0.2.98/en.md new file mode 100644 index 0000000..38236fd --- /dev/null +++ b/docs/release-notes/v0.2.98/en.md @@ -0,0 +1,5 @@ +## Added + +- The version management page now shows software release notes and the latest version's publication date. +- Release notes can be expanded or collapsed, with a link to the full release page. +- Release notes follow the interface language. A message is shown when notes in that language are unavailable. diff --git a/docs/release-notes/v0.2.98/ja.md b/docs/release-notes/v0.2.98/ja.md new file mode 100644 index 0000000..815e296 --- /dev/null +++ b/docs/release-notes/v0.2.98/ja.md @@ -0,0 +1,5 @@ +## 新機能 + +- バージョン管理ページに更新内容を追加しました。最新版の変更点と公開日を確認できます。 +- 更新内容の展開と折りたたみに対応し、リリースページへのリンクを追加しました。 +- 更新内容は画面の言語に合わせて表示されます。その言語の内容がない場合はメッセージを表示します。 diff --git a/docs/release-notes/v0.2.98/zh-CN.md b/docs/release-notes/v0.2.98/zh-CN.md new file mode 100644 index 0000000..bb8a7f9 --- /dev/null +++ b/docs/release-notes/v0.2.98/zh-CN.md @@ -0,0 +1,5 @@ +## 新增 + +- 版本管理页新增软件更新说明,可直接查看最新版的更新内容和发布日期。 +- 更新说明支持展开与收起,并提供完整发布说明入口。 +- 更新说明随界面语言切换;当前语言的内容暂缺时直接显示提示。 diff --git a/docs/release-notes/v0.2.98/zh-TW.md b/docs/release-notes/v0.2.98/zh-TW.md new file mode 100644 index 0000000..ecc654a --- /dev/null +++ b/docs/release-notes/v0.2.98/zh-TW.md @@ -0,0 +1,5 @@ +## 新增 + +- 版本管理頁新增軟體更新說明,可直接查看最新版的更新內容和發布日期。 +- 更新說明支援展開與收合,並提供完整發布說明入口。 +- 更新說明隨介面語言切換;缺少目前語言的內容時會直接顯示提示。 diff --git a/package.json b/package.json index b51334e..7e57dd2 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "@tauri-apps/plugin-dialog": "^2.7.1", "lucide-react": "^1.24.0", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0" }, "devDependencies": { "@tauri-apps/cli": "^2.0.0", diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index 3654331..8761f34 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -3,6 +3,7 @@ import { readFile, stat, writeFile } from 'node:fs/promises'; import { basename, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; import { validateAppVersion } from './version.mjs'; +import { validateReleaseNotes } from './release-notes.mjs'; export function portableUpdateManifestName(platform) { const normalizedPlatform = String(platform).trim().toLowerCase(); @@ -31,6 +32,7 @@ export async function generatePortableUpdateManifest({ gitcodeRepository, tag: rawTag, publishedAt = new Date().toISOString(), + releaseNotes, }) { const resolvedDirectory = resolve(directory ?? 'artifacts'); const platformSpecs = { @@ -62,6 +64,7 @@ export async function generatePortableUpdateManifest({ if (Number.isNaN(Date.parse(publishedAt))) { throw new Error(`Invalid publishedAt: ${publishedAt}`); } + if (releaseNotes !== undefined) validateReleaseNotes(releaseNotes); const assets = {}; for (const arch of ['amd64', 'aarch64']) { @@ -91,6 +94,7 @@ export async function generatePortableUpdateManifest({ version, publishedAt, releaseUrl: `https://github.com/${resolvedRepository}/releases/tag/${tag}`, + ...(releaseNotes === undefined ? {} : { releaseNotes }), assets, }; @@ -116,6 +120,9 @@ async function main() { ? [resolve(requestedOutput)] : portableUpdateManifestNames(platform).map((name) => join(directory, name)); const publishedAt = new Date().toISOString(); + const releaseNotes = args.has('--release-notes') + ? JSON.parse(await readFile(resolve(args.get('--release-notes')), 'utf8')) + : undefined; let manifest; for (const output of outputs) { manifest = await generatePortableUpdateManifest({ @@ -126,6 +133,7 @@ async function main() { gitcodeRepository: args.get('--gitcode-repository'), tag: args.get('--tag'), publishedAt, + releaseNotes, }); } console.log( diff --git a/scripts/release-notes.mjs b/scripts/release-notes.mjs new file mode 100644 index 0000000..7f65433 --- /dev/null +++ b/scripts/release-notes.mjs @@ -0,0 +1,63 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { validateAppVersion } from './version.mjs'; + +export const releaseNotesLanguages = [ + { locale: 'zh-CN', label: '简体中文', missing: '暂未提供该版本的简体中文更新说明。' }, + { locale: 'zh-TW', label: '繁體中文', missing: '暫未提供此版本的繁體中文更新說明。' }, + { locale: 'en', label: 'English', missing: 'Release notes for this version are not available in English.' }, + { locale: 'ja', label: '日本語', missing: 'このバージョンの日本語の更新内容はありません。' }, +]; + +// Only explicitly authored translations are published. Missing languages stay missing. +export async function resolveReleaseNotes({ root = process.cwd(), tag }) { + const version = validateAppVersion(String(tag ?? '').trim().replace(/^v/, ''), 'release tag'); + const directory = join(root, 'docs', 'release-notes', `v${version}`); + const notes = {}; + for (const { locale } of releaseNotesLanguages) { + try { + const text = (await readFile(join(directory, `${locale}.md`), 'utf8')).trim(); + if (text) notes[locale] = `${text}\n`; + } catch (error) { + if (error.code !== 'ENOENT') throw error; + } + } + return notes; +} + +export function validateReleaseNotes(notes) { + if (!notes || typeof notes !== 'object' || Array.isArray(notes)) { + throw new Error('Release notes must be an object keyed by locale'); + } + for (const [locale, text] of Object.entries(notes)) { + if (!releaseNotesLanguages.some((language) => language.locale === locale) + || typeof text !== 'string' || !text.trim()) { + throw new Error(`Invalid release notes for locale: ${locale}`); + } + } +} + +export function releaseNotesBody(notes) { + validateReleaseNotes(notes); + return `${releaseNotesLanguages.map(({ locale, label, missing }) => ( + `# ${label}\n\n${notes[locale]?.trim() || missing}` + )).join('\n\n---\n\n')}\n`; +} + +async function main() { + const args = new Map(); + for (let index = 2; index < process.argv.length; index += 2) { + args.set(process.argv[index], process.argv[index + 1]); + } + if (!args.get('--output') || !args.get('--body-output')) { + throw new Error('--output and --body-output are required'); + } + const notes = await resolveReleaseNotes({ tag: args.get('--tag') }); + await writeFile(resolve(args.get('--output')), `${JSON.stringify(notes, null, 2)}\n`, 'utf8'); + await writeFile(resolve(args.get('--body-output')), releaseNotesBody(notes), 'utf8'); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + await main(); +} diff --git a/src-tauri/src/app_update.rs b/src-tauri/src/app_update.rs index 18c3850..5351729 100644 --- a/src-tauri/src/app_update.rs +++ b/src-tauri/src/app_update.rs @@ -3,6 +3,28 @@ use super::*; pub(crate) const PORTABLE_UPDATE_HELPER_ACK_FILE: &str = "update-helper-started.ack"; const PORTABLE_UPDATE_HELPER_START_TIMEOUT: Duration = Duration::from_secs(10); +pub(crate) fn deserialize_release_notes<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + // Untagged legacy text and invalid entries cannot identify a translation. + // Treat them as unavailable instead of failing the entire version check. + let mut notes = HashMap::new(); + if let Some(translations) = value.as_object() { + for locale in ["zh-CN", "zh-TW", "en", "ja"] { + if let Some(text) = translations.get(locale).and_then(|value| value.as_str()) { + if !text.trim().is_empty() { + notes.insert(locale.to_string(), text.to_string()); + } + } + } + } + Ok(notes) +} + #[tauri::command] pub(crate) fn get_version_source_settings( gui_config_state: tauri::State<'_, GuiConfigState>, @@ -204,6 +226,8 @@ pub(crate) async fn check_app_update( latest_version, update_available, release_url: manifest.release_url, + release_notes: manifest.release_notes, + published_at: manifest.published_at, auto_update_supported, download_size_bytes: asset.map(|value| value.size_bytes), unsupported_reason, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3de79c5..6cdc9d0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -51,7 +51,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(target_os = "macos")] use std::sync::Arc; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, env, fs, fs::File, io::{self, Read, Seek, SeekFrom, Write}, @@ -460,6 +460,8 @@ struct AppUpdateInfo { latest_version: String, update_available: bool, release_url: String, + release_notes: HashMap, + published_at: String, auto_update_supported: bool, download_size_bytes: Option, unsupported_reason: Option, @@ -472,6 +474,8 @@ struct PortableUpdateManifest { version: String, published_at: String, release_url: String, + #[serde(default, deserialize_with = "deserialize_release_notes")] + release_notes: HashMap, assets: std::collections::HashMap, #[serde(default)] full_assets: Option>, diff --git a/src-tauri/src/tests/app_update.rs b/src-tauri/src/tests/app_update.rs index 8b7f2b9..c5b4194 100644 --- a/src-tauri/src/tests/app_update.rs +++ b/src-tauri/src/tests/app_update.rs @@ -79,6 +79,7 @@ fn portable_update_test_manifest(version: &str) -> PortableUpdateManifest { schema_version: 1, version: version.to_string(), published_at: "2026-07-24T00:00:00.000Z".to_string(), + release_notes: HashMap::new(), release_url: format!( "https://github.com/router-for-me/EasyCLIProxyAPI/releases/tag/v{version}" ), @@ -98,6 +99,40 @@ fn portable_update_test_manifest(version: &str) -> PortableUpdateManifest { } } +#[test] +fn portable_update_manifest_uses_only_explicit_locale_release_notes() { + let mut value = serde_json::json!({ + "schemaVersion": 1, + "version": "1.2.3", + "publishedAt": "2026-09-19T00:00:00Z", + "releaseUrl": "https://github.com/router-for-me/EasyCLIProxyAPI/releases/tag/v1.2.3", + "assets": {} + }); + let legacy: PortableUpdateManifest = serde_json::from_value(value.clone()).unwrap(); + assert!(legacy.release_notes.is_empty()); + value["releaseNotes"] = serde_json::json!({ + "zh-CN": "## 新增\n\n- 更新说明。\n", + "en": "## Added\n\n- Release notes.\n", + "ja": null, + "zh-TW": " ", + "fr": "Unsupported locale" + }); + let with_notes: PortableUpdateManifest = serde_json::from_value(value.clone()).unwrap(); + assert_eq!( + with_notes.release_notes.get("zh-CN").map(String::as_str), + Some("## 新增\n\n- 更新说明。\n") + ); + assert_eq!(with_notes.release_notes.len(), 2); + assert!(with_notes.release_notes.contains_key("en")); + assert!(!with_notes.release_notes.contains_key("ja")); + assert!(!with_notes.release_notes.contains_key("zh-TW")); + for unlocalized in [serde_json::Value::Null, serde_json::json!("Legacy notes")] { + value["releaseNotes"] = unlocalized; + let unavailable: PortableUpdateManifest = serde_json::from_value(value.clone()).unwrap(); + assert!(unavailable.release_notes.is_empty()); + } +} + #[test] fn portable_update_manifest_requires_both_matching_github_assets() { let manifest = portable_update_test_manifest("1.2.3"); diff --git a/src/appUpdate.tsx b/src/appUpdate.tsx index 8c04ea2..10e25c8 100644 --- a/src/appUpdate.tsx +++ b/src/appUpdate.tsx @@ -12,13 +12,15 @@ import { import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; import { Download, RefreshCw } from 'lucide-react'; -import { getCurrentLocale, translate, useI18n } from './i18n'; +import { getCurrentLocale, translate, useI18n, type AppLocale } from './i18n'; export type AppUpdateInfo = { currentVersion: string; latestVersion: string; updateAvailable: boolean; releaseUrl: string; + releaseNotes: Partial> | null; + publishedAt: string; autoUpdateSupported: boolean; downloadSizeBytes: number | null; unsupportedReason: string | null; diff --git a/src/components/AppReleaseNotes.tsx b/src/components/AppReleaseNotes.tsx new file mode 100644 index 0000000..ebe0495 --- /dev/null +++ b/src/components/AppReleaseNotes.tsx @@ -0,0 +1,105 @@ +import { useId, useState } from 'react'; +import { ChevronDown, ExternalLink, FileText } from 'lucide-react'; +import Markdown from 'react-markdown'; +import type { AppUpdateInfo } from '../appUpdate'; +import { useI18n } from '../i18n'; + +type Props = { + info: Pick | null; + checking: boolean; + failed: boolean; + onOpenUrl: (url?: string) => void | Promise; +}; + +export function releaseNotesUrl(value: string): string { + try { + const url = new URL(value); + return ['http:', 'https:'].includes(url.protocol) ? url.href : ''; + } catch { + return ''; + } +} + +export function AppReleaseNotes({ info, checking, failed, onOpenUrl }: Props) { + const { t, formatDate, locale } = useI18n(); + const [expanded, setExpanded] = useState(true); + const contentId = useId(); + const headingId = useId(); + const notes = info?.releaseNotes?.[locale]?.trim(); + const publishedAt = info?.publishedAt && Number.isFinite(Date.parse(info.publishedAt)) + ? info.publishedAt + : null; + const emptyMessage = info + ? t('appUpdate.notes.empty') + : checking + ? t('appUpdate.notes.loading') + : failed + ? t('appUpdate.notes.failed') + : t('appUpdate.notes.notChecked'); + + return ( +
+
+
+
+ +
+ +
+ ); +} diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 61b0f9d..2be7222 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -1394,6 +1394,15 @@ export const jaOverrides = { 'appUpdate.catalog.unchanged': 'モデルカタログは最新です', 'appUpdate.installNow': '今すぐ更新', 'appUpdate.openRelease': 'ダウンロードページを開く', + 'appUpdate.notes.title': 'ソフトウェアの更新内容', + 'appUpdate.notes.expand': '展開', + 'appUpdate.notes.collapse': '折りたたむ', + 'appUpdate.notes.publishedAt': '{date}公開', + 'appUpdate.notes.openRelease': 'リリースノート全体を見る', + 'appUpdate.notes.empty': '現在の言語の更新内容を取得できませんでした。', + 'appUpdate.notes.loading': '更新内容を取得中…', + 'appUpdate.notes.failed': '更新内容を取得できませんでした。後でもう一度確認してください。', + 'appUpdate.notes.notChecked': 'ソフトウェアの更新を確認すると、最新版の更新内容が表示されます。', 'appUpdate.confirmTitle': 'アプリ更新をインストール', 'appUpdate.confirmDescription': '{version} をダウンロードしてインストールします。アプリは再起動し、管理中のコアは一時停止後に復帰します。', 'appUpdate.progressTitle': 'アプリを更新中', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 5558a89..a1192f4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1394,6 +1394,15 @@ export const en: Record = { 'appUpdate.catalog.unchanged': 'Model catalog is already up to date', 'appUpdate.installNow': 'Update Now', 'appUpdate.openRelease': 'Open Download Page', + 'appUpdate.notes.title': 'Release notes', + 'appUpdate.notes.expand': 'Expand', + 'appUpdate.notes.collapse': 'Collapse', + 'appUpdate.notes.publishedAt': 'Published {date}', + 'appUpdate.notes.openRelease': 'View full release notes', + 'appUpdate.notes.empty': 'Release notes are not available in the current language.', + 'appUpdate.notes.loading': 'Loading release notes…', + 'appUpdate.notes.failed': 'Unable to load release notes. Please check again later.', + 'appUpdate.notes.notChecked': 'Check for software updates to see the latest release notes.', 'appUpdate.confirmTitle': 'Install Application Update', 'appUpdate.confirmDescription': 'Version {version} will be downloaded and installed. The app will restart, and a managed core will briefly stop and resume.', 'appUpdate.progressTitle': 'Updating Application', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 7ea72e8..47bbf44 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -1393,6 +1393,15 @@ export const zhCN = { 'appUpdate.catalog.unchanged': '模型目录已是最新版本', 'appUpdate.installNow': '立即升级', 'appUpdate.openRelease': '打开下载页面', + 'appUpdate.notes.title': '软件更新说明', + 'appUpdate.notes.expand': '展开', + 'appUpdate.notes.collapse': '收起', + 'appUpdate.notes.publishedAt': '{date}发布', + 'appUpdate.notes.openRelease': '查看完整发布说明', + 'appUpdate.notes.empty': '暂未获取到当前语言的更新说明。', + 'appUpdate.notes.loading': '正在获取更新说明…', + 'appUpdate.notes.failed': '暂时无法获取更新说明,请稍后重新检查。', + 'appUpdate.notes.notChecked': '检查软件更新后,将显示最新版的更新说明。', 'appUpdate.confirmTitle': '安装软件更新', 'appUpdate.confirmDescription': '将下载并安装 {version}。应用会自动重启,正在运行的托管内核会短暂停止并恢复。', 'appUpdate.progressTitle': '正在升级软件', diff --git a/src/i18n/resources.ts b/src/i18n/resources.ts index 1a9296d..79a982c 100644 --- a/src/i18n/resources.ts +++ b/src/i18n/resources.ts @@ -7,5 +7,16 @@ export { en, zhCN }; export type { MessageKey }; export type MessageVariables = Record; -export const zhTW: Record = createTraditionalMessages(zhCN); +export const zhTW: Record = { + ...createTraditionalMessages(zhCN), + 'appUpdate.notes.title': '軟體更新說明', + 'appUpdate.notes.expand': '展開', + 'appUpdate.notes.collapse': '收合', + 'appUpdate.notes.publishedAt': '{date}發布', + 'appUpdate.notes.openRelease': '查看完整發布說明', + 'appUpdate.notes.empty': '尚未取得目前語言的更新說明。', + 'appUpdate.notes.loading': '正在取得更新說明…', + 'appUpdate.notes.failed': '暫時無法取得更新說明,請稍後重新檢查。', + 'appUpdate.notes.notChecked': '檢查軟體更新後,將顯示最新版的更新說明。', +}; export const ja: Record = jaOverrides; diff --git a/src/pages/VersionManagementPage.tsx b/src/pages/VersionManagementPage.tsx index 67fd12f..4f83576 100644 --- a/src/pages/VersionManagementPage.tsx +++ b/src/pages/VersionManagementPage.tsx @@ -16,6 +16,7 @@ import { useI18n } from '../i18n'; import { useAppUpdate } from '../appUpdate'; import { MessageNotice, FloatingNotice, useAppNotice } from '../appNotice'; import { createVersionManagementVisitTracker } from '../services/versionManagementVisits'; +import { AppReleaseNotes } from '../components/AppReleaseNotes'; export type CoreInstallResult = { version: string; @@ -319,9 +320,9 @@ export function VersionManagementPage() { setCancellingInstall(false); }; - const openAppRelease = async () => { + const openAppRelease = async (url = appUpdate?.releaseUrl || APP_RELEASE_URL) => { try { - await invoke('open_external_url', { url: appUpdate?.releaseUrl || APP_RELEASE_URL }); + await invoke('open_external_url', { url }); } catch (error) { showNotice({ key: 'kernel.error.openUpdate', variables: { error: String(error) } }, 'error'); } @@ -711,6 +712,13 @@ export function VersionManagementPage() { + {customMirrorDialogOpen ? ( diff --git a/src/styles.css b/src/styles.css index e6eb57a..eec3410 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1124,7 +1124,8 @@ textarea:focus-visible { } .version-source-row, -.version-list-item { +.version-list-item, +.app-release-notes { border: 1px solid var(--theme-e3e1db); border-radius: 14px; background: var(--theme-fffdf8); @@ -1382,6 +1383,180 @@ textarea:focus-visible { white-space: nowrap; } +.app-release-notes { + min-width: 0; + overflow: hidden; +} + +.app-release-notes-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 20px 26px; +} + +.app-release-notes-heading > div { + display: flex; + align-items: center; + gap: 9px; + min-width: 0; +} + +.app-release-notes-heading h2 { + margin: 0; + font-size: var(--font-size-panel-title); +} + +.app-release-notes-heading svg { + flex: none; + color: var(--theme-7a746c); +} + +.app-release-notes-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + flex: none; + min-height: 32px; + padding: 3px 0; + border: 0; + background: transparent; + color: var(--theme-6d6760); + font-size: var(--font-size-meta); + cursor: pointer; +} + +.app-release-notes-toggle:hover { + color: var(--theme-2d2a26); +} + +.app-release-notes-toggle .is-expanded { + transform: rotate(180deg); +} + +.app-release-notes-body { + padding: 0 26px 20px; +} + +.app-release-notes-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 9px; + padding-bottom: 20px; + border-bottom: 1px solid var(--theme-e8e5dc); +} + +.app-release-notes-meta strong { + font-family: var(--font-family-mono, monospace); + font-size: var(--font-size-panel-title); + overflow-wrap: anywhere; +} + +.app-release-notes-latest { + padding: 2px 7px; + border-radius: 5px; + background: var(--theme-dce8df); + color: var(--theme-2f6b3f); + font-size: var(--font-size-meta); +} + +.app-release-notes-meta time { + margin-left: auto; + color: var(--theme-7a746c); + font-size: var(--font-size-meta); +} + +.app-release-notes-markdown { + display: grid; + grid-template-columns: minmax(44px, auto) minmax(0, 1fr); + align-items: start; + gap: 18px 22px; + margin-top: 20px; + color: var(--theme-2d2a26); + font-size: var(--font-size-body); + line-height: var(--line-height-body); + overflow-wrap: anywhere; +} + +.app-release-notes-markdown > * { + grid-column: 2; + min-width: 0; + margin: 0; +} + +.app-release-notes-markdown > .release-notes-category { + grid-column: 1; + max-width: 160px; + padding: 3px 9px; + border-radius: 5px; + background: var(--theme-faf9f5); + color: var(--theme-6d6760); + font-size: var(--font-size-meta); + font-weight: 600; + text-align: center; +} + +.app-release-notes-markdown > .release-notes-subtitle { + grid-column: 1 / -1; +} + +.app-release-notes-markdown ul, +.app-release-notes-markdown ol { + padding-left: 20px; +} + +.app-release-notes-markdown li + li { + margin-top: 7px; +} + +.app-release-notes-markdown li p { + margin: 5px 0; +} + +.app-release-notes-markdown a { + color: var(--theme-3f6f98); + text-decoration: underline; +} + +.app-release-notes-markdown pre { + white-space: pre-wrap; + padding: 12px; + border-radius: 8px; + background: var(--theme-faf9f5); +} + +.app-release-notes-markdown blockquote { + border-left: 3px solid var(--theme-e3e1db); + padding-left: 14px; + color: var(--theme-6d6760); +} + +.app-release-notes-empty { + margin: 18px 0 0; + color: var(--theme-7a746c); + font-size: var(--font-size-body); +} + +.app-release-notes-footer { + display: flex; + justify-content: flex-end; + padding: 10px 26px; + border-top: 1px solid var(--theme-e8e5dc); + background: var(--theme-faf9f5); +} + +@media (max-width: 640px) { + .app-release-notes-heading { padding: 16px; } + .app-release-notes-body { padding: 0 16px 16px; } + .app-release-notes-footer { padding: 10px 16px; } + .app-release-notes-markdown { gap: 14px 12px; } + .app-release-notes-markdown > .release-notes-category { max-width: 80px; } + .app-release-notes-meta time { width: 100%; margin-left: 0; } +} + .client-api-panel { margin-top: 0; } diff --git a/tests/appReleaseNotes.test.tsx b/tests/appReleaseNotes.test.tsx new file mode 100644 index 0000000..1a4c4e1 --- /dev/null +++ b/tests/appReleaseNotes.test.tsx @@ -0,0 +1,100 @@ +import { describe, expect, test } from 'bun:test'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { AppReleaseNotes, releaseNotesUrl } from '../src/components/AppReleaseNotes'; +import { I18nProvider, supportedLocales, translate, type AppLocale } from '../src/i18n'; + +const translations = { + 'zh-CN': '## 新增\n\n- 支持 **更新说明**。\n', + 'zh-TW': '## 新增\n\n- 支援 **更新說明**。\n', + en: '## Added\n\n- Supports **release notes**.\n', + ja: '## 新機能\n\n- **更新内容**を表示します。\n', +}; +const info = { + latestVersion: '0.2.98', + publishedAt: '2026-09-19T00:00:00Z', + releaseNotes: translations, +}; + +function renderNotes(overrides: Partial[0]> = {}, locale: AppLocale = 'zh-CN') { + const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window'); + // The provider reads the saved UI language on mount; effects do not run during SSR. + Object.defineProperty(globalThis, 'window', { + configurable: true, value: { localStorage: { getItem: () => locale } }, + }); + try { + return renderToStaticMarkup( + + {}} {...overrides} /> + , + ); + } finally { + if (previousWindow) Object.defineProperty(globalThis, 'window', previousWindow); + else Reflect.deleteProperty(globalThis, 'window'); + } +} + +describe('localized release notes display', () => { + test('renders versioned Markdown with date and release link without requiring an available update', () => { + const html = renderNotes(); + expect(html).toContain('v0.2.98'); + expect(html).toMatch(/