diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ffd5861..7f539ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,8 +23,57 @@ permissions: contents: write jobs: - build-and-package: + prepare-release: runs-on: ubuntu-latest + outputs: + raw_tag: ${{ steps.prepare.outputs.raw_tag }} + version: ${{ steps.prepare.outputs.version }} + targets_json: ${{ steps.prepare.outputs.targets_json }} + channel_variants: ${{ steps.prepare.outputs.channel_variants }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve release inputs + id: prepare + shell: bash + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then + raw_tag="${{ inputs.tag }}" + elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + raw_tag="${GITHUB_REF_NAME}" + else + raw_tag="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" + fi + + build_targets="${{ inputs.build_targets || '' }}" + channel_variants="${{ inputs.channel_variants || '' }}" + if [ -z "$build_targets" ]; then + build_targets="linux/amd64 linux/arm64 linux/riscv64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64" + fi + if [ -z "$channel_variants" ]; then + channel_variants="full none telegram discord feishu maixcam qq dingtalk whatsapp" + fi + + version="${raw_tag#v}" + targets_json="$(python3 -c 'import json, sys; print(json.dumps(sys.argv[1].split()))' "$build_targets")" + + echo "raw_tag=$raw_tag" >> "$GITHUB_OUTPUT" + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "targets_json=$targets_json" >> "$GITHUB_OUTPUT" + echo "channel_variants=$channel_variants" >> "$GITHUB_OUTPUT" + echo "Build version: $version (from $raw_tag)" + echo "Targets: $build_targets" + echo "Variants: $channel_variants" + + build-and-package: + needs: prepare-release + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: ${{ fromJson(needs.prepare-release.outputs.targets_json) }} steps: - name: Checkout uses: actions/checkout@v4 @@ -47,74 +96,74 @@ jobs: sudo apt-get update sudo apt-get install -y zip - - name: Resolve build version - id: ver + - name: Resolve matrix target + id: target + shell: bash run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then - raw="${{ inputs.tag }}" - elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then - raw="${GITHUB_REF_NAME}" - else - raw="$(git describe --tags --always --dirty 2>/dev/null || echo dev)" - fi - version="${raw#v}" - echo "raw=$raw" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "Build version: $version (from $raw)" + set -euo pipefail + target="${{ matrix.target }}" + goos="${target%/*}" + goarch="${target#*/}" + echo "goos=$goos" >> "$GITHUB_OUTPUT" + echo "goarch=$goarch" >> "$GITHUB_OUTPUT" + echo "artifact_name=release-$goos-$goarch" >> "$GITHUB_OUTPUT" - - name: Build and package release artifacts + - name: Build and package matrix artifacts + shell: bash run: | set -euo pipefail make clean - build_targets="${{ inputs.build_targets || '' }}" - channel_variants="${{ inputs.channel_variants || '' }}" - if [ -z "$build_targets" ]; then - build_targets="linux/amd64 linux/arm64 linux/riscv64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64" - fi - if [ -z "$channel_variants" ]; then - channel_variants="full none telegram discord feishu maixcam qq dingtalk whatsapp" - fi - echo "BUILD_TARGETS=$build_targets" - echo "CHANNEL_PACKAGE_VARIANTS=$channel_variants" - make package-all VERSION="${{ steps.ver.outputs.version }}" BUILD_TARGETS="$build_targets" CHANNEL_PACKAGE_VARIANTS="$channel_variants" + make package-all \ + VERSION="${{ needs.prepare-release.outputs.version }}" \ + BUILD_TARGETS="${{ matrix.target }}" \ + CHANNEL_PACKAGE_VARIANTS="${{ needs.prepare-release.outputs.channel_variants }}" + rm -f build/checksums.txt - - name: Upload artifacts + - name: Upload matrix artifacts uses: actions/upload-artifact@v4 with: - name: release-artifacts - path: build + name: ${{ steps.target.outputs.artifact_name }} + path: | + build/*.tar.gz + build/*.zip if-no-files-found: error publish-release: - needs: build-and-package + needs: + - prepare-release + - build-and-package runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' steps: - - name: Download artifacts + - name: Download packaged artifacts uses: actions/download-artifact@v4 with: - name: release-artifacts + pattern: release-* + merge-multiple: true path: build - name: List downloaded artifacts - run: find build -maxdepth 4 -type f | sort + run: find build -maxdepth 2 -type f | sort - - name: Resolve tag - id: tag + - name: Generate checksums + shell: bash run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ inputs.tag }}" ]; then - echo "name=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + set -euo pipefail + cd build + if command -v sha256sum >/dev/null 2>&1; then + sha256sum *.tar.gz *.zip > checksums.txt else - echo "name=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + shasum -a 256 *.tar.gz *.zip > checksums.txt fi + cat checksums.txt - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: ${{ steps.tag.outputs.name }} - name: ${{ steps.tag.outputs.name }} + tag_name: ${{ needs.prepare-release.outputs.raw_tag }} + name: ${{ needs.prepare-release.outputs.raw_tag }} generate_release_notes: true files: | - build/**/*.tar.gz - build/**/*.zip - build/**/checksums.txt + build/*.tar.gz + build/*.zip + build/checksums.txt diff --git a/webui/src/components/RecursiveConfig.tsx b/webui/src/components/RecursiveConfig.tsx index a197ca2..6c7d85e 100644 --- a/webui/src/components/RecursiveConfig.tsx +++ b/webui/src/components/RecursiveConfig.tsx @@ -186,9 +186,6 @@ const RecursiveConfig: React.FC = ({ data, labels, path = onChange={(e) => onChange(currentPath, e.target.checked)} className="w-4 h-4 rounded border-zinc-700 text-indigo-500 focus:ring-indigo-500" /> - - {value ? (labels['enabled_true'] || t('enabled_true')) : (labels['enabled_false'] || t('enabled_false'))} - ) : ( string; @@ -28,8 +28,83 @@ function getWhatsAppBooleanIcon(fieldKey: string) { } } -function formatList(value: unknown) { - return Array.isArray(value) ? value.join('\n') : ''; +type TagListFieldProps = { + isWhatsApp: boolean; + onChange: (values: string[]) => void; + placeholder?: string; + value: unknown; +}; + +function TagListField({ isWhatsApp, onChange, placeholder, value }: TagListFieldProps) { + const values = Array.isArray(value) ? value.map((item) => String(item || '').trim()).filter(Boolean) : []; + const [draft, setDraft] = React.useState(''); + + React.useEffect(() => { + setDraft(''); + }, [value]); + + function commit(raw: string) { + const items = String(raw || '') + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + if (items.length === 0) { + setDraft(''); + return; + } + const next = [...values]; + items.forEach((item) => { + if (!next.includes(item)) next.push(item); + }); + onChange(next); + setDraft(''); + } + + function remove(item: string) { + onChange(values.filter((value) => value !== item)); + } + + return ( +
+ {values.length > 0 ? ( +
+ {values.map((item) => ( + + ))} +
+ ) : null} + setDraft(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + commit(draft); + } else if (e.key === 'Backspace' && !draft && values.length > 0) { + e.preventDefault(); + remove(values[values.length - 1]); + } + }} + onBlur={() => { + if (draft.trim()) commit(draft); + }} + placeholder={placeholder || ''} + monospace={isWhatsApp} + /> +
+ ); } const ChannelFieldRenderer: React.FC = ({ @@ -61,9 +136,6 @@ const ChannelFieldRenderer: React.FC = ({
{helper}
-
- {t(value ? 'enabled_true' : 'enabled_false')} -
= ({