diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml index 84a29a2..5d1b923 100644 --- a/.github/workflows/build-images.yml +++ b/.github/workflows/build-images.yml @@ -1,72 +1,11 @@ -name: Build Dashboard Images - -on: - workflow_call: - inputs: - push_images: - description: "Push service images instead of local builds" - type: boolean - default: true - - dockerhub_namespace: - description: "Docker Hub namespace (user/org)" - type: string - - skip_security: - description: "Skip security scans and signing" - type: boolean - default: false - - node_builder_image: - type: string - default: "node:22-bookworm" - - node_runtime_image: - type: string - default: "node:22-slim" - - workflow_dispatch: - inputs: - push_images: - type: boolean - default: true - - dockerhub_namespace: - description: "Docker Hub namespace (user/org)" - type: string - default: "cloudneutral" - - skip_security: - description: "Skip security scans and signing" - type: boolean - default: false - - node_builder_image: - type: string - default: "node:22-bookworm" - - node_runtime_image: - type: string - default: "node:22-slim" - - push: - branches: [ main ] - -permissions: - contents: read - packages: write - id-token: write +name: Build Multi-Arch Images env: REGISTRY: ghcr.io - # ✅ 不硬编码:默认推到 ghcr.io/<当前仓库 owner>/... ORG: ${{ github.repository_owner }} - SKIP_SECURITY: ${{ inputs.skip_security || github.event.inputs.skip_security || 'false' }} - NODE_BUILDER_IMAGE: ${{ inputs.node_builder_image || github.event.inputs.node_builder_image || 'node:22-bookworm' }} NODE_RUNTIME_IMAGE: ${{ inputs.node_runtime_image || github.event.inputs.node_runtime_image || 'node:22-slim' }} - PUSH_IMAGES: ${{ github.event_name == 'push' || (github.event_name == 'workflow_call' && inputs.push_images) || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true') }} @@ -86,7 +25,7 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + - uses: ./.github/actions/docker-login with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -98,15 +37,15 @@ jobs: with: image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }} - - uses: docker/setup-qemu-action@v3 - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + - uses: ./.github/actions/docker-setup-qemu + - uses: ./.github/actions/docker-setup-buildx - name: Clone knowledge content run: git clone https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge - name: Build Service Image (per-arch) id: build - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 + uses: ./.github/actions/docker-build-push with: context: ${{ matrix.service.workdir }} file: ${{ matrix.service.dockerfile }} @@ -131,144 +70,3 @@ jobs: with: name: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }} path: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }}.txt - - security: - runs-on: ubuntu-latest - needs: build - if: ${{ (github.event_name == 'push' || inputs.push_images == true || github.event.inputs.push_images == 'true') && !((inputs.skip_security == true) || (github.event.inputs.skip_security == 'true')) }} - - strategy: - matrix: - arch: - - { platform: linux/amd64, artifact: linux-amd64 } - - { platform: linux/arm64, artifact: linux-arm64 } - service: - - { name: dashboard } - - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - - uses: actions/download-artifact@v4 - with: - name: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }} - - - name: Load image digest - env: - DIGEST_FILE: digest-${{ matrix.service.name }}-${{ matrix.arch.artifact }}.txt - run: bash .github/scripts/build-images/load-image-digest.sh - - - name: Set image ref - env: - SERVICE_NAME: ${{ matrix.service.name }} - IMAGE_SHA: ${{ github.sha }} - IMAGE_ARTIFACT: ${{ matrix.arch.artifact }} - run: bash .github/scripts/build-images/set-image-ref.sh - - - uses: anchore/sbom-action@v0 - with: - image: ${{ env.IMG }} - output-file: sbom.spdx.json - - - uses: actions/upload-artifact@v4 - with: - name: sbom-${{ matrix.service.name }}-${{ matrix.arch.artifact }} - path: sbom.spdx.json - - - uses: aquasecurity/trivy-action@0.28.0 - with: - image-ref: ${{ env.IMG }} - severity: HIGH,CRITICAL - exit-code: '1' - - - uses: sigstore/cosign-installer@v3 - with: - cosign-release: 'v2.4.1' - - - name: Cosign Sign Image - env: - COSIGN_EXPERIMENTAL: "true" - run: | - set -euo pipefail - cosign sign --yes "${{ env.IMG }}" - - push: - runs-on: ubuntu-latest - needs: - - build - - security - if: ${{ needs.build.result == 'success' && (github.event_name == 'push' || inputs.push_images == true || github.event.inputs.push_images == 'true') && ((inputs.skip_security == true) || (github.event.inputs.skip_security == 'true') || (needs.security.result == 'success')) }} - - strategy: - fail-fast: false - matrix: - registry: - - ghcr.io - - docker.io - - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - - - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f - - - uses: actions/download-artifact@v4 - with: - name: digest-dashboard-linux-amd64 - - - uses: actions/download-artifact@v4 - with: - name: digest-dashboard-linux-arm64 - - - name: Load digests - env: - AMD_DIGEST_FILE: digest-dashboard-linux-amd64.txt - ARM_DIGEST_FILE: digest-dashboard-linux-arm64.txt - run: bash .github/scripts/build-images/load-manifest-digests.sh - - - name: Generate Auto Tags - id: meta - uses: ./.github/actions/auto-tag - with: - image: ${{ env.REGISTRY }}/${{ env.ORG }}/dashboard - - - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - if: matrix.registry == 'ghcr.io' - with: - registry: ${{ matrix.registry }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Create & Push Multi-Arch Manifests (GHCR) - if: matrix.registry == 'ghcr.io' - env: - IMAGE_SHA: ${{ github.sha }} - TAGS_CSV: ${{ steps.meta.outputs.tags }} - run: bash .github/scripts/build-images/create-ghcr-manifest.sh - - - name: Clone knowledge content - if: matrix.registry == 'ghcr.io' - run: git clone https://github.com/Cloud-Neutral-Workshop/knowledge.git knowledge - - - name: Validate blog content mount - if: matrix.registry == 'ghcr.io' - env: - IMAGE: ${{ env.REGISTRY }}/${{ env.ORG }}/dashboard@${{ env.MANIFEST_DIGEST }} - KNOWLEDGE_CONTENT_DIR: ${{ github.workspace }}/knowledge/content - run: bash .github/scripts/build-images/validate-blog-content-mount.sh - - - name: Login to Docker Hub - if: matrix.registry == 'docker.io' - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 - with: - registry: docker.io - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Copy Multi-Arch Image to Docker Hub (skopeo) - if: matrix.registry == 'docker.io' - env: - TARGET_NS: ${{ inputs.dockerhub_namespace || github.event.inputs.dockerhub_namespace || 'cloudneutral' }} - GHCR_USERNAME: ${{ github.actor }} - GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} - DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - run: bash .github/scripts/build-images/copy-image-to-dockerhub.sh diff --git a/.github/workflows/check-image.yaml b/.github/workflows/check-image.yaml index 9475579..8c37f19 100644 --- a/.github/workflows/check-image.yaml +++ b/.github/workflows/check-image.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Authenticate to GHCR - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 + uses: ./.github/actions/docker-login with: registry: ghcr.io username: ${{ github.actor }} diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/xworkmate/page.tsx b/src/app/xworkmate/page.tsx index 9bc8730..c02fdd9 100644 --- a/src/app/xworkmate/page.tsx +++ b/src/app/xworkmate/page.tsx @@ -1,58 +1,22 @@ export const dynamic = "force-dynamic"; import { Suspense } from "react"; -import { headers } from "next/headers"; import { XWorkmateLoading } from "@/app/xworkmate/XWorkmateLoading"; -import { XWorkmateWorkspacePage } from "@/components/xworkmate/XWorkmateWorkspacePage"; -import { - buildXWorkmateScopeKey, - toXWorkmateIntegrationDefaults, -} from "@/lib/xworkmate/types"; +import { XWorkmateWorkspaceRoute } from "@/components/xworkmate/XWorkmateWorkspaceRoute"; import { getConsoleIntegrationDefaults } from "@/server/consoleIntegrations"; -import { getXWorkmateSessionContext } from "@/server/xworkmate/profile"; export const metadata = { title: "XWorkmate", description: "Online XWorkmate workspace powered by OpenClaw gateway", }; -export default async function XWorkmatePage({ - searchParams, -}: { - searchParams?: Promise<{ prompt?: string; sessionKey?: string }>; -}) { - const requestHeaders = await headers(); - const requestHost = requestHeaders.get("host"); - const { user, profile } = await getXWorkmateSessionContext(requestHost); - const defaults = - profile ? toXWorkmateIntegrationDefaults(profile) : getConsoleIntegrationDefaults(); - const scopeKey = buildXWorkmateScopeKey( - profile, - user?.id ?? user?.uuid ?? null, - requestHost, - ); - const resolvedSearchParams = searchParams ? await searchParams : undefined; - const initialPrompt = - typeof resolvedSearchParams?.prompt === "string" - ? resolvedSearchParams.prompt - : ""; - const initialSessionKey = - typeof resolvedSearchParams?.sessionKey === "string" - ? resolvedSearchParams.sessionKey - : ""; - +export default function XWorkmatePage() { + const defaults = getConsoleIntegrationDefaults(); return (
}> - +
); diff --git a/src/components/xworkmate/XWorkmateWorkspaceRoute.tsx b/src/components/xworkmate/XWorkmateWorkspaceRoute.tsx new file mode 100644 index 0000000..f18fb50 --- /dev/null +++ b/src/components/xworkmate/XWorkmateWorkspaceRoute.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; + +import type { IntegrationDefaults } from "@/lib/openclaw/types"; +import { useUserStore } from "@/lib/userStore"; +import { normalizeXWorkmateHost } from "@/lib/xworkmate/host"; +import { + buildXWorkmateScopeKey, + toXWorkmateIntegrationDefaults, + type XWorkmateProfileResponse, +} from "@/lib/xworkmate/types"; +import { XWorkmateWorkspacePage } from "@/components/xworkmate/XWorkmateWorkspacePage"; + +type XWorkmateWorkspaceRouteProps = { + defaults: IntegrationDefaults; +}; + +async function fetchProfile(): Promise { + const response = await fetch("/api/xworkmate/profile", { + credentials: "include", + cache: "no-store", + headers: { + Accept: "application/json", + }, + }); + + if (response.status === 401) { + return null; + } + + if (!response.ok) { + throw new Error(`xworkmate_profile_failed:${response.status}`); + } + + return (await response.json()) as XWorkmateProfileResponse; +} + +export function XWorkmateWorkspaceRoute({ + defaults, +}: XWorkmateWorkspaceRouteProps): React.ReactNode { + const searchParams = useSearchParams(); + const sessionUser = useUserStore((state) => state.user); + const [profile, setProfile] = useState(null); + + const requestHost = useMemo(() => { + if (typeof window === "undefined") { + return ""; + } + + return normalizeXWorkmateHost(window.location.host); + }, []); + + useEffect(() => { + let cancelled = false; + + async function loadProfile() { + try { + const nextProfile = await fetchProfile(); + if (!cancelled) { + setProfile(nextProfile); + } + } catch (error) { + console.error("Failed to load xworkmate profile", error); + if (!cancelled) { + setProfile(null); + } + } + } + + void loadProfile(); + + return () => { + cancelled = true; + }; + }, []); + + const resolvedDefaults = profile + ? toXWorkmateIntegrationDefaults(profile) + : defaults; + const scopeKey = buildXWorkmateScopeKey( + profile, + sessionUser?.id ?? sessionUser?.uuid ?? null, + requestHost, + ); + const initialPrompt = searchParams.get("prompt") ?? ""; + const initialSessionKey = searchParams.get("sessionKey") ?? ""; + + return ( + + ); +}