{diff.object_type}
{diff.current_content ?? diff.base_content ?? ''}diff --git a/.dockerignore b/.dockerignore index 7c1057c..9decaca 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,6 @@ bin coverage node_modules var -web/dist -web/node_modules +apps/web/dist +apps/web/node_modules *.log diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ada8cd2..26ad7bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,12 +42,60 @@ jobs: - run: go vet ./... - run: go test -race ./... - run: pnpm architecture + - run: pnpm docs:links - run: pnpm governance:v3 - run: pnpm governance:content - run: pnpm evaluate:plugin - run: pnpm test:plugin-signing - run: pnpm check:plugin - - run: pnpm --dir web typecheck - - run: pnpm --dir web test - - run: pnpm --dir web build + - run: pnpm --dir apps/web typecheck + - run: pnpm --dir apps/web test + - run: pnpm --dir apps/web build + - run: pnpm --dir apps/desktop typecheck + - run: pnpm --dir apps/desktop test + - run: pnpm --dir apps/desktop package + - run: xvfb-run -a pnpm --dir apps/desktop exec playwright test + - run: pnpm desktop:release:check + - run: pnpm test:desktop-release - run: node --check packages/contentcloud/bin/contentcloud.js + + desktop-package-matrix: + name: desktop package (${{ matrix.target }}) + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + os: macos-14 + platform: darwin + arch: arm64 + - target: darwin-x64 + os: macos-14 + platform: darwin + arch: x64 + - target: win32-x64 + os: windows-latest + platform: win32 + arch: x64 + - target: linux-x64 + os: ubuntu-latest + platform: linux + arch: x64 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10.8.1 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm desktop:release:check + - run: pnpm --dir apps/desktop exec electron-forge package --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} + - uses: actions/upload-artifact@v4 + with: + name: content-work-os-desktop-${{ matrix.target }} + path: apps/desktop/out/** + if-no-files-found: error diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 0000000..26349d6 --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,434 @@ +name: Desktop Release + +on: + push: + tags: + - "desktop-v*" + workflow_dispatch: + inputs: + version: + description: "版本号或 desktop-v 标签,例如 0.28.0-beta.1" + required: true + source_ref: + description: "构建来源 ref;留空使用 main" + required: false + default: "" + channel: + description: "更新通道;稳定版只能使用已签名产物" + required: true + type: choice + options: + - beta + - stable + default: beta + publish: + description: "是否创建或更新 GitHub Release;手动运行默认只上传 CI artifact" + required: true + type: boolean + default: false + +permissions: + contents: write + actions: read + +concurrency: + group: desktop-release-${{ github.ref }} + cancel-in-progress: false + +env: + NODE_VERSION: "22" + PNPM_VERSION: "10.8.1" + ELECTRON_CACHE: ${{ github.workspace }}/.cache/electron + +jobs: + resolve: + name: Resolve desktop release identity + runs-on: ubuntu-22.04 + outputs: + tag: ${{ steps.identity.outputs.tag }} + version: ${{ steps.identity.outputs.version }} + channel: ${{ steps.identity.outputs.channel }} + source_ref: ${{ steps.identity.outputs.source_ref }} + publish: ${{ steps.identity.outputs.publish }} + steps: + - name: Resolve tag, version and channel + id: identity + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + EVENT_TAG: ${{ github.ref_name }} + INPUT_VERSION: ${{ github.event.inputs.version || '' }} + INPUT_CHANNEL: ${{ github.event.inputs.channel || 'beta' }} + INPUT_SOURCE_REF: ${{ github.event.inputs.source_ref || '' }} + INPUT_PUBLISH: ${{ github.event.inputs.publish || 'false' }} + run: | + set -euo pipefail + if [[ "$EVENT_NAME" == "push" ]]; then + tag="$EVENT_TAG" + source_ref="${GITHUB_SHA}" + publish=true + else + raw_version="${INPUT_VERSION#desktop-v}" + tag="desktop-v${raw_version}" + source_ref="${INPUT_SOURCE_REF:-main}" + publish="${INPUT_PUBLISH}" + fi + + if [[ ! "$tag" =~ ^desktop-v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::invalid desktop release tag: $tag" + exit 1 + fi + version="${tag#desktop-v}" + if [[ "$version" == *-* ]]; then + channel=beta + elif [[ "$EVENT_NAME" == push ]]; then + channel=stable + else + channel="${INPUT_CHANNEL:-stable}" + fi + if [[ "$channel" == stable && "$version" == *-* ]]; then + echo "::error::a prerelease version cannot enter stable" + exit 1 + fi + if [[ "$publish" == true && "$channel" == beta && "$version" != *-* ]]; then + echo "::error::a published beta requires a prerelease version" + exit 1 + fi + if [[ "$publish" != true && "$publish" != false ]]; then + echo "::error::publish must be true or false" + exit 1 + fi + + { + echo "tag=$tag" + echo "version=$version" + echo "channel=$channel" + echo "source_ref=$source_ref" + echo "publish=$publish" + } >> "$GITHUB_OUTPUT" + + build: + name: Build ${{ matrix.name }} + needs: resolve + strategy: + fail-fast: false + matrix: + include: + - target: darwin-arm64 + name: macOS arm64 + runner: macos-14 + platform: darwin + arch: arm64 + forge_targets: dmg,zip + signing: required + - target: darwin-x64 + name: macOS x64 + runner: macos-15-intel + platform: darwin + arch: x64 + forge_targets: dmg,zip + signing: required + - target: win32-x64 + name: Windows x64 + runner: windows-2022 + platform: win32 + arch: x64 + forge_targets: squirrel + signing: required + - target: linux-x64 + name: Linux x64 + runner: ubuntu-22.04 + platform: linux + arch: x64 + forge_targets: deb,rpm + signing: preview + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.resolve.outputs.source_ref }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build macOS DMG native dependency + if: matrix.platform == 'darwin' + shell: bash + run: | + set -euo pipefail + for package_name in macos-alias fs-xattr; do + if [[ "$package_name" == macos-alias ]]; then + expected_output=volume.node + else + expected_output=xattr.node + fi + package_json="$(find node_modules/.pnpm -path "*/node_modules/$package_name/package.json" -print -quit)" + if [[ -z "$package_json" ]]; then + echo "::error::$package_name package is missing" + exit 1 + fi + package_dir="$(dirname "$package_json")" + pnpm exec node-gyp rebuild --directory "$package_dir" + test -f "$package_dir/build/Release/$expected_output" + done + + - name: Validate desktop release contract + run: pnpm desktop:release:check + + - name: Verify package version matches release tag + shell: bash + env: + RELEASE_VERSION: ${{ needs.resolve.outputs.version }} + run: | + set -euo pipefail + package_version="$(node -p "require('./apps/desktop/package.json').version")" + test "$package_version" = "$RELEASE_VERSION" || { + echo "::error::apps/desktop/package.json is $package_version, expected $RELEASE_VERSION" + exit 1 + } + + - name: Validate macOS signing secrets + if: matrix.platform == 'darwin' + shell: bash + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + set -euo pipefail + missing=() + for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_SIGNING_IDENTITY APPLE_TEAM_ID KEYCHAIN_PASSWORD; do + [[ -n "${!name:-}" ]] || missing+=("$name") + done + if (( ${#missing[@]} )); then + echo "::error::missing macOS signing/notarization secrets: ${missing[*]}" + exit 1 + fi + + - name: Import macOS signing certificate + if: matrix.platform == 'darwin' + shell: bash + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + set -euo pipefail + keychain="$RUNNER_TEMP/contentcloud-desktop-signing.keychain-db" + certificate="$RUNNER_TEMP/contentcloud-desktop-signing.p12" + echo "$APPLE_CERTIFICATE" | base64 --decode > "$certificate" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain" + security import "$certificate" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain" + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$keychain" + security list-keychains -d user -s "$keychain" + echo "CONTENTCLOUD_DESKTOP_MAC_KEYCHAIN=$keychain" >> "$GITHUB_ENV" + + - name: Validate Windows signing secrets + if: matrix.platform == 'win32' + shell: bash + env: + WINDOWS_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE }} + WINDOWS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + [[ -n "${WINDOWS_SIGNING_CERTIFICATE:-}" ]] || { echo "::error::missing WINDOWS_SIGNING_CERTIFICATE"; exit 1; } + [[ -n "${WINDOWS_SIGNING_CERTIFICATE_PASSWORD:-}" ]] || { echo "::error::missing WINDOWS_SIGNING_CERTIFICATE_PASSWORD"; exit 1; } + + - name: Prepare Windows signing certificate + if: matrix.platform == 'win32' + shell: bash + env: + WINDOWS_SIGNING_CERTIFICATE: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE }} + WINDOWS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + certificate="$RUNNER_TEMP/contentcloud-desktop-signing.pfx" + CERTIFICATE_PATH="$certificate" node --input-type=module <<'NODE' + import { writeFileSync } from 'node:fs'; + writeFileSync(process.env.CERTIFICATE_PATH, Buffer.from(process.env.WINDOWS_SIGNING_CERTIFICATE, 'base64')); + NODE + echo "CONTENTCLOUD_DESKTOP_WINDOWS_CERTIFICATE_FILE=$certificate" >> "$GITHUB_ENV" + echo "CONTENTCLOUD_DESKTOP_WINDOWS_CERTIFICATE_PASSWORD=$WINDOWS_SIGNING_CERTIFICATE_PASSWORD" >> "$GITHUB_ENV" + + - name: Build Electron packages + shell: bash + env: + CONTENTCLOUD_DESKTOP_SIGN: ${{ matrix.signing == 'required' && '1' || '' }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + set -euo pipefail + pnpm --dir apps/desktop exec electron-forge make \ + --platform "${{ matrix.platform }}" \ + --arch "${{ matrix.arch }}" \ + --targets "${{ matrix.forge_targets }}" + + - name: Verify macOS signatures and notarization + if: matrix.platform == 'darwin' + shell: bash + run: | + set -euo pipefail + apps=() + while IFS= read -r -d '' app; do apps+=("$app"); done < <(find apps/desktop/out -type d -name '*.app' -not -path '*.app/*' -print0) + if (( ${#apps[@]} == 0 )); then + echo "::error::Electron Forge produced no macOS app bundle" + exit 1 + fi + for app in "${apps[@]}"; do + codesign --verify --deep --strict --verbose=4 "$app" + spctl --assess --type execute --verbose=4 "$app" + done + + - name: Verify Windows Authenticode signature + if: matrix.platform == 'win32' + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $installers = Get-ChildItem "apps/desktop/out/make" -Recurse -File -Filter "*.exe" + if ($installers.Count -eq 0) { + throw "Electron Forge produced no Windows Squirrel installer" + } + foreach ($installer in $installers) { + $signature = Get-AuthenticodeSignature $installer.FullName + if ($signature.Status -ne "Valid") { + throw "Invalid Authenticode signature for $($installer.FullName): $($signature.Status)" + } + } + + - name: Stage release assets and generate metadata + shell: bash + env: + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} + RELEASE_VERSION: ${{ needs.resolve.outputs.version }} + RELEASE_CHANNEL: ${{ needs.resolve.outputs.channel }} + RELEASE_REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + signing_flag=() + if [[ "${{ matrix.signing }}" == required ]]; then signing_flag+=(--signed); fi + node scripts/stage-desktop-release.mjs stage \ + --forge-dir apps/desktop/out \ + --out-dir "desktop-staged/${{ matrix.target }}" \ + --target "${{ matrix.target }}" \ + --version "$RELEASE_VERSION" \ + --channel "$RELEASE_CHANNEL" \ + --tag "$RELEASE_TAG" \ + --repository "$RELEASE_REPOSITORY" \ + "${signing_flag[@]}" + + - name: Upload staged desktop assets + uses: actions/upload-artifact@v4 + with: + name: desktop-release-${{ matrix.target }} + path: desktop-staged/${{ matrix.target }}/* + if-no-files-found: error + retention-days: 14 + + publish: + name: Publish GitHub desktop release + needs: [resolve, build] + if: needs.resolve.outputs.publish == 'true' && needs.build.result == 'success' + runs-on: ubuntu-22.04 + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ needs.resolve.outputs.source_ref }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Download staged desktop assets + uses: actions/download-artifact@v4 + with: + pattern: desktop-release-* + path: desktop-staged + merge-multiple: true + + - name: Aggregate release metadata and checksums + shell: bash + env: + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} + RELEASE_CHANNEL: ${{ needs.resolve.outputs.channel }} + RELEASE_REPOSITORY: ${{ github.repository }} + run: | + set -euo pipefail + mkdir -p github-release-assets + node scripts/stage-desktop-release.mjs aggregate \ + --input-dir desktop-staged \ + --out-dir github-release-assets \ + --channel "$RELEASE_CHANNEL" \ + --tag "$RELEASE_TAG" \ + --repository "$RELEASE_REPOSITORY" \ + --require-all-targets + cp desktop-staged/* github-release-assets/ + find github-release-assets -maxdepth 1 -type f -print | sort + + - name: Create draft GitHub Release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} + SOURCE_REF: ${{ needs.resolve.outputs.source_ref }} + RELEASE_CHANNEL: ${{ needs.resolve.outputs.channel }} + shell: bash + run: | + set -euo pipefail + if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --target "$SOURCE_REF" --draft=true + else + gh release create "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$SOURCE_REF" \ + --title "Content Work OS Desktop $RELEASE_TAG" \ + --generate-notes \ + --draft + fi + + - name: Upload desktop release assets + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} + shell: bash + run: | + set -euo pipefail + gh release upload "$RELEASE_TAG" github-release-assets/* --repo "$GITHUB_REPOSITORY" --clobber + + - name: Publish release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} + RELEASE_CHANNEL: ${{ needs.resolve.outputs.channel }} + shell: bash + run: | + set -euo pipefail + if [[ "$RELEASE_CHANNEL" == beta ]]; then + gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --prerelease + else + gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest + fi diff --git a/.gitignore b/.gitignore index 6195947..cd73c21 100644 --- a/.gitignore +++ b/.gitignore @@ -5,13 +5,16 @@ dist/ coverage/ node_modules/ -web/dist/ -web/node_modules/ +apps/*/dist/ +apps/*/node_modules/ +apps/*/.vite/ +apps/*/out/ +apps/*/test-results/ var/ .contentcloud/ *.tsbuildinfo -web/vite.config.js -web/vite.config.d.ts +apps/web/vite.config.js +apps/web/vite.config.d.ts *.log .playwright-mcp/ register-create.png diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d67f374 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted diff --git a/CHANGELOG.md b/CHANGELOG.md index b968700..19c380f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ ContentCloud 的重要变更记录在此文件中。 +## [0.28.0] - 2026-08-17 + +### Added + +- 增加 Provider Profile、Provider Binding 和管理员 Provider 管理页面,支持按租户配置出口策略、预算和服务商路由。 +- Provider 凭据引用只允许写入,不在 API 响应中回显,并增加对应 HTTP/应用层回归测试。 + +### Changed + +- Content Work OS 前端迁移到 `apps/web`,同步 Workspace、Docker、CI、治理脚本和开发文档路径。 +- 更新工作台、Studio、运营页和官网视觉层级,统一 Provider 状态与白标工作流文案。 +- 应用发布版本与独立签名的 Scene Plugin 版本解耦;应用升级到 `0.28.0`,生产插件继续固定在已验证的 `0.27.0`。 + +### Security + +- Provider Binding 查询不再泄漏 `credential_ref`,仅返回是否已配置凭据。 + ## [0.27.0] - 2026-08-15 ### Added diff --git a/DESIGN.md b/DESIGN.md index 3985743..6734804 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -9,7 +9,7 @@ ## Visual Baseline -- `web/src/marketing/marketing.css` 中当前首页是品牌视觉基准。首页以白色为主表面,用不对称的品牌叙事和真实任务工作面组成首屏;自然光创作空间只承载右侧场景氛围,不能压过产品品牌、任务状态和客户下一步。 +- `apps/web/src/marketing/marketing.css` 中当前首页是品牌视觉基准。首页首屏使用浅青绿像素场景、居中品牌叙事和真实任务摘要组成单一构图;产品品牌、任务状态和客户下一步保持主导。 - 工作台、平台后台、登录注册、文档和公开页面复用首页的中性灰白、清亮蓝焦点、Logo、字体与图标语言,但不复制营销页的大标题或内容编排。 - 工作面以白色、浅灰背景、中性边框和严格网格承载高频操作;近黑色用于主要命令,清亮蓝只用于品牌、选中态、链接和焦点,侧栏保持白色,不得形成大面积蓝色区域。 - 颜色用于区分品牌交互、对象类型和系统状态。品牌主题以白色为主、明亮蓝为辅,不使用粉色主题,也不把整个界面染成单一蓝色。 @@ -99,8 +99,8 @@ ### Public Website -- 首屏以白色为主表面,用品牌标题与真实任务工作面形成单一构图;继续使用现有 `/images/content-work-os-studio.webp` 自然光场景,不替换图片、不生成新背景,也不增加蓝色滤镜。 -- Hero 保持品牌优先、单一 CTA 组和下一段露出;任务画布承载固定输入、人工确认、版本状态和唯一下一步,不做漂浮节点或纯装饰流程图。 +- 首屏使用独立的浅青绿像素场景 `/images/content-work-os-pixel-field.webp`,采用居中品牌标题、双 CTA、四周白色像素片段和底部任务摘要露出的构图;登录注册继续使用 `/images/content-work-os-studio.webp` 自然光场景。 +- Hero 保持品牌优先,像素片段只承担轻量场景氛围;下一段任务摘要承载固定输入、人工确认、版本状态和唯一下一步,不使用与业务无关的漂浮节点或装饰流程图。 - 首页紧接客户创作流程、结果资产目录、资产复用闭环、三执行平面和能力状态表,视觉内容必须来自产品文档与当前代码事实。 - 首页内容状态来自 Registry 和文档目录,不静态承诺未实现能力。 @@ -154,3 +154,4 @@ | 2026-08-11 | 官网能力叙事以 Infra 文档和公开目录为事实源 | 明确区分本地可用、服务端可用、执行框架已接通与外部依赖,避免把底层实现写成真实账号或自动发布已接通。 | | 2026-08-12 | 对标参考站的中性工作室层级,蓝色只保留为焦点 | 用户否定大面积皇家蓝和高饱和天蓝;采用白色主表面、浅灰工作区、近黑命令、清亮蓝焦点,并将 Studio 调整为 `112px` 紧凑导航、约 `404px` 上下文栏和开放工作面。 | | 2026-08-12 | 采用参考站的白色留白、胶囊控件和柔和浮层,品牌色改为冰川蓝 | 用户再次否定普通企业蓝观感;颜色只替代参考站的粉色强调,不把页面整体染蓝,官网保留原自然光背景图,登录、Studio 与后台共享同一套冷白表面和墨色命令。 | +| 2026-08-17 | 首页改为 Catpaw 同类居中绿色像素场景 | 复用透明导航、居中 Hero、白色像素片段、双 CTA 和下一段露出的构图与动效机制;不复制对方品牌素材、Logo 或文案,继续使用 Content Work OS 的真实任务内容。 | diff --git a/Dockerfile.server b/Dockerfile.server index b24cdb5..c8d26e6 100644 --- a/Dockerfile.server +++ b/Dockerfile.server @@ -2,11 +2,11 @@ FROM node:22-bookworm-slim AS web-build WORKDIR /src RUN corepack enable COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -COPY web/package.json web/package.json +COPY apps/web/package.json apps/web/package.json COPY packages/contentcloud/package.json packages/contentcloud/package.json RUN pnpm install --frozen-lockfile -COPY web web -RUN pnpm --dir web build +COPY apps/web apps/web +RUN pnpm --dir apps/web build FROM golang:1.24-bookworm AS go-build WORKDIR /src @@ -24,7 +24,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates RUN useradd --system --uid 10001 --create-home contentcloud WORKDIR /app COPY --from=go-build /out/contentcloud-server /usr/local/bin/contentcloud-server -COPY --from=web-build /src/web/dist /app/web/dist +COPY --from=web-build /src/apps/web/dist /app/web/dist USER contentcloud ENV CONTENTCLOUD_ADDR=:8080 CONTENTCLOUD_WEB_DIST=/app/web/dist CONTENTCLOUD_DEV_MODE=0 EXPOSE 8080 diff --git a/Makefile b/Makefile index eae006f..a420a01 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -.PHONY: dev preview server web worker cli build test check check-plugin install-cli migrate-up +.PHONY: dev preview server web desktop worker cli build build-web package-desktop make-desktop test test-desktop test-desktop-e2e check check-plugin install-cli migrate-up CONTENTCLOUD_ADDR ?= :8080 -CONTENTCLOUD_WEB_DIST ?= web/dist +CONTENTCLOUD_WEB_DIST ?= apps/web/dist dev: ./scripts/dev.sh @@ -13,7 +13,10 @@ server: go run ./cmd/contentcloud-server web: - pnpm --dir web dev + pnpm --dir apps/web dev + +desktop: + pnpm --dir apps/desktop start worker: go run ./cmd/contentcloud-worker @@ -28,23 +31,42 @@ build: build-web go build -o bin/contentcloud ./cmd/contentcloud build-web: - pnpm --dir web build + pnpm --dir apps/web build + +package-desktop: + pnpm --dir apps/desktop package + +make-desktop: + pnpm --dir apps/desktop make test: go test ./... - pnpm --dir web test + pnpm --dir apps/web test + pnpm --dir apps/desktop test + +test-desktop: + pnpm --dir apps/desktop typecheck + pnpm --dir apps/desktop test + +test-desktop-e2e: + pnpm --dir apps/desktop test:e2e check: go fmt ./... go vet ./... go test ./... pnpm architecture + pnpm docs:links pnpm governance:v3 pnpm governance:content pnpm test:plugin-signing pnpm check:plugin - pnpm --dir web typecheck - pnpm --dir web build + pnpm --dir apps/web typecheck + pnpm --dir apps/web test + pnpm --dir apps/web build + pnpm --dir apps/desktop typecheck + pnpm --dir apps/desktop test + pnpm --dir apps/desktop package check-plugin: pnpm evaluate:plugin diff --git a/README.md b/README.md index 768c991..65171e1 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,13 @@ Content Work OS 是面向 AI 内容营销团队的 Studio-first 创作与云端 ```text cmd/ Go Server、Worker、CLI/Daemon -internal/ Domain、Application Service、Transport 与 Adapter +internal/ Runtime、Local、Transport、Integration、Persistence 与业务模块 contracts/ OpenAPI 3.1 与 JSON Schema migrations/ PostgreSQL schema、RLS 与 runtime role skills/ 随 CLI 内嵌的本地 Agent Skill packages/contentcloud/ 零依赖 npm 校验安装器 -web/ React + TypeScript 工作台与客户审批页 +apps/web/ React + TypeScript 工作台与客户审批页 +apps/desktop/ Electron 持续项目工作面 docs/foundation/ 平台基线、分层规范、迁移与工程门禁 docs/product/ 客户产品需求与平台叙事 docs/roadmap/v8/ Agentic Job Runtime 专项路线图 @@ -50,6 +51,14 @@ make dev 开发模式下,演示账号默认具备平台管理员权限,并自动创建金陵古法线香演示项目;来源由内置确定性 Worker 处理。 +Desktop 通过本机 Go Daemon 的版本化 Desktop API 获取项目目录、同步/上传、审批和 Runtime/Delivery 投影,不复用 Codex 渲染面。启动开发版: + +```bash +pnpm dev:desktop +``` + +`pnpm package:desktop` 生成当前平台未签名安装目录;正式签名、notarization 和自动更新仍是发布门禁,不属于当前已发布能力。 + CLI 示例: ```bash @@ -150,7 +159,9 @@ project: "品牌 / 单品" ```bash make check go test -race ./... -pnpm --dir web test +pnpm --dir apps/web test +pnpm test:desktop +pnpm test:e2e:desktop pnpm check:plugin ``` @@ -160,7 +171,7 @@ PostgreSQL 集成测试必须使用可重建的专用开发库;推荐先创建 ```bash CONTENTCLOUD_TEST_DATABASE_URL='postgres://contentcloud@127.0.0.1:5432/<专用测试库>?sslmode=disable' \ -GOMAXPROCS=2 go test -race -p 1 -count=1 ./internal/store/postgres +GOMAXPROCS=2 go test -race -p 1 -count=1 ./internal/persistence/postgres ``` ## 当前实现事实 diff --git a/VERSION b/VERSION index 1b58cc1..697f087 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.27.0 +0.28.0 diff --git a/apps/desktop/forge.config.ts b/apps/desktop/forge.config.ts new file mode 100644 index 0000000..3eb0968 --- /dev/null +++ b/apps/desktop/forge.config.ts @@ -0,0 +1,120 @@ +import type { ForgeConfig } from "@electron-forge/shared-types"; +import { FusesPlugin } from "@electron-forge/plugin-fuses"; +import { VitePlugin } from "@electron-forge/plugin-vite"; +import { FuseV1Options, FuseVersion } from "@electron/fuses"; +import { MakerDeb } from "@electron-forge/maker-deb"; +import { MakerDMG } from "@electron-forge/maker-dmg"; +import { MakerRpm } from "@electron-forge/maker-rpm"; +import { MakerSquirrel } from "@electron-forge/maker-squirrel"; +import { MakerZIP } from "@electron-forge/maker-zip"; + +const releaseSigningEnabled = process.env.CONTENTCLOUD_DESKTOP_SIGN === "1"; + +function macSignOptions() { + if (process.platform !== "darwin" || !releaseSigningEnabled) { + return undefined; + } + + const identity = process.env.APPLE_SIGNING_IDENTITY?.trim(); + if (!identity) { + throw new Error( + "APPLE_SIGNING_IDENTITY is required for a signed desktop release", + ); + } + + return { + identity, + keychain: process.env.CONTENTCLOUD_DESKTOP_MAC_KEYCHAIN, + hardenedRuntime: true, + gatekeeperAssess: true, + identityValidation: true, + }; +} + +function macNotarizeOptions() { + if (process.platform !== "darwin" || !releaseSigningEnabled) { + return undefined; + } + + const appleId = process.env.APPLE_ID?.trim(); + const appleIdPassword = process.env.APPLE_APP_SPECIFIC_PASSWORD?.trim(); + const teamId = process.env.APPLE_TEAM_ID?.trim(); + if (!appleId || !appleIdPassword || !teamId) { + throw new Error( + "APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD and APPLE_TEAM_ID are required for notarization", + ); + } + + return { appleId, appleIdPassword, teamId }; +} + +function squirrelOptions() { + if (process.platform !== "win32" || !releaseSigningEnabled) { + return {}; + } + + const certificateFile = + process.env.CONTENTCLOUD_DESKTOP_WINDOWS_CERTIFICATE_FILE?.trim(); + const certificatePassword = + process.env.CONTENTCLOUD_DESKTOP_WINDOWS_CERTIFICATE_PASSWORD; + if (!certificateFile || !certificatePassword) { + throw new Error( + "CONTENTCLOUD_DESKTOP_WINDOWS_CERTIFICATE_FILE and CONTENTCLOUD_DESKTOP_WINDOWS_CERTIFICATE_PASSWORD are required for a signed desktop release", + ); + } + + return { certificateFile, certificatePassword }; +} + +const config: ForgeConfig = { + packagerConfig: { + asar: true, + name: "Content Work OS", + executableName: "content-work-os", + appBundleId: "run.zhongcao.contentcloud.desktop", + protocols: [{ name: "Content Work OS", schemes: ["contentcloud"] }], + osxSign: macSignOptions(), + osxNotarize: macNotarizeOptions(), + win32metadata: { + CompanyName: "ContentCloud", + ProductName: "Content Work OS", + FileDescription: "ContentCloud project workspace desktop", + }, + }, + rebuildConfig: {}, + makers: [ + new MakerSquirrel(squirrelOptions()), + new MakerZIP({}, ["darwin"]), + new MakerDMG({}), + new MakerDeb({}), + new MakerRpm({}), + ], + plugins: [ + new VitePlugin({ + build: [ + { + entry: "src/main/main.ts", + config: "vite.main.config.ts", + target: "main", + }, + { + entry: "src/preload/preload.ts", + config: "vite.preload.config.ts", + target: "preload", + }, + ], + renderer: [{ name: "main_window", config: "vite.renderer.config.ts" }], + }), + new FusesPlugin({ + version: FuseVersion.V1, + [FuseV1Options.RunAsNode]: false, + [FuseV1Options.EnableCookieEncryption]: true, + [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, + [FuseV1Options.EnableNodeCliInspectArguments]: false, + [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, + [FuseV1Options.OnlyLoadAppFromAsar]: true, + }), + ], +}; + +export default config; diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 0000000..ea0f56c --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,13 @@ + + +
+ + + +{project.last_synced_at ? `最近同步 ${formatDate(project.last_synced_at)}` : `本地 Revision ${project.local_revision} · Cloud Revision ${project.cloud_revision}`}
+ + {result ?{inbox.data.message}
当前项目没有待处理的提交内容版本。
{detail.revision.content_hash} · {formatDate(detail.revision.created_at)}
{diff.current_content ?? diff.base_content ?? ''}{item.body}
{item.json_pointer || 'Revision'} · {formatDate(item.created_at)}{detail}
Codex 负责任务期推理、生成与工具执行;Desktop 只展示 Daemon 与 Cloud Runtime 的可恢复投影。
交付只能引用已批准快照和内容摘要,Desktop 不渲染 Codex 对话内容。
{message}
本地 Daemon 尚未提供项目绑定。
{change.path}{formatDiffValue(change.before)}没有需要绑定的能力。
: preview.capabilities.map(item =>当前没有客户环境事实;不会填充演示绑定。
: preview.environments.map(item =>没有需要绑定的功能。
: preview.capabilities.map(item =>当前没有客户设置记录;不会填充演示连接。
: preview.environments.map(item =>{item.registered_versions.length ? item.registered_versions.join('、') : '未登记'}{item.ready ? '可绑定' : item.missing_capabilities.length ? `缺少 ${item.missing_capabilities.length} 项` : '未登记'}{item.registered_versions.length ? item.registered_versions.join('、') : '未登记'}{item.ready ? '可连接' : item.missing_capabilities.length ? `缺少 ${item.missing_capabilities.length} 项` : '未登记'}{version.digest||'暂未返回记录'}{capability.id}{capability.input_schema||'未登记'}{capability.output_schema||'未登记'}{capability.digest||'未登记'}{skill.id}{skill.digest}{skill.source.repository}{skill.source.ref}{skill.signature.algorithm} / {skill.signature.key_id}{skill.evaluation.report||'未登记'}{skill.evaluation.digest||'未登记'}{notice}
} + {newDeviceToken&&{newDeviceToken}{executor.id}{executor.daemon_instance_id||'尚未上报'}{executor.hostname||'未登记'}{workspace.workspace_id}{workspace.project_id}{workspace.generation||'尚未上报'}{description}
{platformAdmin?'维护视频制作服务的版本、发布状态和客户连接设置。':'查看当前客户可以使用的视频制作服务,并维护连接设置。'}
{profile.digest}先查看客户任务当前停在哪里,再决定是否暂停、恢复或核对外部结果。
查看客户任务停在哪一步,并在需要时暂停、恢复或核对结果。
{summary.project_name || '未命名项目'}{summary.product_name ? ` · ${summary.product_name} v${summary.product_version}` : ''} · 更新于 {dateTime(summary.updated_at)}
{summary.source_job_run_id &&{summary.task_next_action || '任务详情由服务端实时汇总。'}
{summary.id}{summary.task_next_action || '任务详情会在这里汇总。'}
{summary.id}{error}
}{catalog.home.description}
能力状态直接来自智能客户端目录。
只有完整实现的组合才提供可执行的场景教程。
这里的状态表示每种连接工具目前能做到什么。
只有已经准备好的内容形式,才会提供可以照着做的指南。
把资料和已有成果带进任务。系统组织证据、知识、Agent、模型、Worker 和人工确认,产出可审核、可交付、可复用的内容结果。
-资料、策略、创作和审核持续接力,把每次内容成果沉淀为下一次任务可以复用的资产。
+资料、项目参考、当前进度、候选结果和人工门禁收敛在一项创作任务里;每一步都显示阻断原因和唯一推荐动作。
+资料、项目参考、当前进度、候选结果和确认事项都集中在一项创作任务里;每一步都会告诉你为什么停下,以及接下来该做什么。
公开场景状态来自文档目录。底层能力已实现,不代表外部平台账号、配额或自动发布已经接通。
+这里显示的是当前真正可以使用的内容场景。系统已有基础功能,不代表外部平台账号、额度或自动发布已经接通。
{content.summary}
查看内容形态类型化契约、本地校验和渠道基础能力正在形成;只有完整客户路径经过验证后才会进入公开可用目录。
查看能力边界内容规则、基础检查和渠道功能正在准备;只有完整客户路径经过验证后才会进入公开可用目录。
查看可用范围选择已验证的内容场景,在需要判断的地方做决定,其余执行、版本和追溯交给系统。
@@ -196,12 +260,12 @@ export function MarketingHome() {人物、剧本、分镜、图片和视频结果按版本、确认状态与使用关系统一呈现。
+团队资料和创作结果会分别整理;加入任务时会记住使用的版本和来源,不会悄悄改动原文。
+电脑负责需要本地处理的内容,平台负责提交、审核和保存确认结果,外部服务负责搜索、生成或发布;每一步的责任都清楚可查。
+{capability.facts}
+ {capability.boundary}
+资料、参考内容、制作步骤和人工确认会一起协作;候选结果经过来源、质量和使用权限检查后,才会成为可以交付的内容。
+{session.message}
新建一个会话,并选择用于这个项目的本地目录。
Codex 会先检查环境,再请你回到此页确认电脑。
{prompt}{session.verification_code}只有 Codex 中显示相同代码时,才确认连接。
{session.support_code}>}{session.support_code}}{session.message}
新建一个会话,并选择这个项目所在的文件夹。
连接工具会检查电脑准备情况,然后请你回到此页确认。
{prompt}{session.verification_code}只有电脑上的连接工具显示相同代码时,才确认连接。
{session.support_code}>}{session.support_code}}{notice}
}{error&&{error}
}{message}
{message}
查找结果分为可以使用、暂时不能使用和还缺少的内容。查找不会修改知识内容,也不会把暂时不能使用的内容当作可用依据。
{item.id} · v{item.version}{snapshot.digest.slice(0,18)}…{value.id} · v{value.version}{value.statement}
{value.governance_message||'当前没有可执行的审核动作,此版本仅供查看。'}
{description}
返回终端继续完成登录即可,此页面可以关闭。
>:<>输入终端显示的一次性用户代码。本次授权只会建立当前用户的命令行会话,不会授予本地智能客户端凭据。
回到电脑上的连接工具继续登录即可,此页面可以关闭。
>:<>输入电脑上显示的一次性代码。这里只会把这台电脑连接到当前账号,不会授予其他权限。
本次决定已绑定到该内容版本的摘要,并写入审计记录。
你的决定已保存到这份内容的当前版本,并留下记录。
输入与该链接分开发送的六位验证码。
输入与该链接分开发送的六位验证码。
{session.tenant.name} 使用固定角色,撤销成员权限后其会话会立即失效。
{session.tenant.name} 使用预设角色,移除成员后其登录会立即失效。
{createdToken}{createdToken}{mode === 'create' ? '注册账号并创建你的内容团队' : '凭管理员发放的邀请令牌加入现有团队'}
+{mode === 'create' ? '注册账号并创建你的内容团队' : '使用管理员发来的邀请码加入现有团队'}