merge console frontend release

This commit is contained in:
Haitao Pan 2026-03-18 23:59:17 +08:00
commit 34231e29ed
5 changed files with 111 additions and 249 deletions

View File

@ -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

View File

@ -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 }}

2
next-env.d.ts vendored
View File

@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
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.

View File

@ -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 (
<div className="h-[calc(100vh-var(--app-shell-nav-offset))] w-full">
<Suspense fallback={<XWorkmateLoading />}>
<XWorkmateWorkspacePage
defaults={defaults}
profile={profile}
initialPrompt={initialPrompt}
initialSessionKey={initialSessionKey}
requestHost={requestHost ?? undefined}
scopeKey={scopeKey}
/>
<XWorkmateWorkspaceRoute defaults={defaults} />
</Suspense>
</div>
);

View File

@ -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<XWorkmateProfileResponse | null> {
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<XWorkmateProfileResponse | null>(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 (
<XWorkmateWorkspacePage
defaults={resolvedDefaults}
profile={profile}
initialPrompt={initialPrompt}
initialSessionKey={initialSessionKey}
requestHost={requestHost}
scopeKey={scopeKey}
/>
);
}