param(
  [ValidateSet('all', 'xhs', 'douyin')]
  [string]$ProjectId = 'all',
  [string]$SmokeOutput
)

$ErrorActionPreference = 'Stop'

function Assert-Condition {
  param(
    [bool]$Condition,
    [string]$Message
  )

  if (-not $Condition) {
    throw $Message
  }
}

function Write-Utf8File {
  param(
    [string]$FilePath,
    [string]$Content
  )

  $parent = Split-Path -Parent $FilePath
  if ($parent) {
    New-Item -ItemType Directory -Force -Path $parent | Out-Null
  }
  $encoding = New-Object System.Text.UTF8Encoding $false
  [System.IO.File]::WriteAllText($FilePath, $Content, $encoding)
}

function Decode-Utf8Base64 {
  param([string]$Value)

  return [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Value))
}

function Get-XhsVolcesImageProviderConfig {
  $modelId = 'doubao-seedream-5-0-260128'
  return [ordered]@{
    providerName = 'volces'
    modelId = $modelId
    providers = [ordered]@{
      volces = [ordered]@{
        baseUrl = 'https://ark.cn-beijing.volces.com/api/v3/images/generations'
        apiKey = 'smoke-key'
        api = 'openai-completions'
        models = @(
          [ordered]@{
            id = $modelId
            name = 'volces-gen-image'
            reasoning = $false
            input = @('text', 'image')
            cost = [ordered]@{
              input = 0
              output = 0
              cacheRead = 0
              cacheWrite = 0
            }
            contextWindow = 128000
            maxTokens = 81920
          }
        )
      }
    }
  }
}

function Get-XhsTextProviderFixtureConfig {
  $modelId = 'qwen3.5-plus'
  return [ordered]@{
    providerName = 'qwen'
    modelId = $modelId
    providers = [ordered]@{
      qwen = [ordered]@{
        baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
        apiKey = 'smoke-key'
        api = 'openai-completions'
        models = @(
          [ordered]@{
            id = $modelId
            input = @('text')
          }
        )
      }
      modelstudio = [ordered]@{
        baseUrl = 'https://coding.dashscope.aliyuncs.com/v1'
        api = 'openai-completions'
        models = @(
          [ordered]@{
            id = 'qwen3.5-plus'
            input = @('text', 'image')
          }
        )
      }
      'openclaw-cloud' = [ordered]@{
        baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
        apiKey = 'smoke-key'
        api = 'openai-completions'
        models = @(
          [ordered]@{
            id = 'kimi-k2.5'
            input = @('text', 'image')
          }
        )
      }
    }
    primaryModel = 'openclaw-cloud/kimi-k2.5'
  }
}

function Get-NormalizedRelativePath {
  param(
    [string]$BasePath,
    [string]$TargetPath
  )

  $pathType = [System.IO.Path]
  if ($pathType.GetMethod('GetRelativePath', [type[]]@([string], [string]))) {
    $relative = [System.IO.Path]::GetRelativePath($BasePath, $TargetPath)
  } else {
    $baseUri = New-Object System.Uri(((Resolve-Path $BasePath).Path.TrimEnd('\') + '\'))
    $targetUri = New-Object System.Uri((Resolve-Path $TargetPath).Path)
    $relative = [System.Uri]::UnescapeDataString($baseUri.MakeRelativeUri($targetUri).ToString())
  }
  return ($relative -replace '\\', '/').TrimStart('./')
}

function Test-ExcludedRelativePath {
  param(
    [string]$RelativePath,
    [string[]]$ExcludePatterns,
    [bool]$IsDirectory
  )

  $normalized = ($RelativePath -replace '\\', '/').Trim('/')
  $candidateValues = @($normalized)
  if ($IsDirectory -and $normalized) {
    $candidateValues += "$normalized/"
  }

  foreach ($pattern in $ExcludePatterns) {
    $rawPattern = [string]$pattern
    if (-not $rawPattern) {
      continue
    }
    $normalizedPattern = ($rawPattern -replace '\\', '/').Trim()
    if (-not $normalizedPattern) {
      continue
    }
    $wildcard = New-Object System.Management.Automation.WildcardPattern($normalizedPattern, [System.Management.Automation.WildcardOptions]::IgnoreCase)
    foreach ($candidate in $candidateValues) {
      if ($candidate -and $wildcard.IsMatch($candidate)) {
        return $true
      }
    }
    if ($normalized -and -not $normalizedPattern.Contains('/')) {
      $leaf = Split-Path -Leaf $normalized
      if ($leaf -and $wildcard.IsMatch($leaf)) {
        return $true
      }
    }
  }
  return $false
}

function Copy-ProjectPackageSource {
  param(
    [string]$SourceRoot,
    [string]$DestinationRoot
  )

  $projectJsonPath = Join-Path $SourceRoot 'project.json'
  $excludePatterns = @()
  if (Test-Path $projectJsonPath) {
    $projectConfig = Get-Content -Path $projectJsonPath -Raw | ConvertFrom-Json
    if ($projectConfig.bundlePackaging -and $projectConfig.bundlePackaging.excludePaths) {
      $excludePatterns = @($projectConfig.bundlePackaging.excludePaths | ForEach-Object { [string]$_ })
    }
  }

  function Copy-ProjectEntry {
    param(
      [string]$CurrentSource,
      [string]$CurrentDestination
    )

    New-Item -ItemType Directory -Force -Path $CurrentDestination | Out-Null
    foreach ($entry in Get-ChildItem -LiteralPath $CurrentSource -Force) {
      $relativePath = Get-NormalizedRelativePath -BasePath $SourceRoot -TargetPath $entry.FullName
      if (Test-ExcludedRelativePath -RelativePath $relativePath -ExcludePatterns $excludePatterns -IsDirectory $entry.PSIsContainer) {
        continue
      }
      $targetPath = Join-Path $CurrentDestination $entry.Name
      if ($entry.PSIsContainer) {
        Copy-ProjectEntry -CurrentSource $entry.FullName -CurrentDestination $targetPath
      } else {
        New-Item -ItemType Directory -Force -Path (Split-Path -Parent $targetPath) | Out-Null
        Copy-Item -LiteralPath $entry.FullName -Destination $targetPath -Force
      }
    }
  }

  if (Test-Path $DestinationRoot) {
    Remove-Item -LiteralPath $DestinationRoot -Recurse -Force
  }
  Copy-ProjectEntry -CurrentSource $SourceRoot -CurrentDestination $DestinationRoot
}

function Invoke-PythonCommand {
  param(
    [string[]]$Arguments,
    [string]$WorkingDirectory,
    [hashtable]$EnvironmentOverrides = @{}
  )

  $previousValues = @{}
  foreach ($key in $EnvironmentOverrides.Keys) {
    $previousValues[$key] = [Environment]::GetEnvironmentVariable($key, 'Process')
    [Environment]::SetEnvironmentVariable($key, [string]$EnvironmentOverrides[$key], 'Process')
  }

  try {
    $previousErrorActionPreference = $ErrorActionPreference
    $ErrorActionPreference = 'Continue'
    Push-Location $WorkingDirectory
    $rawOutput = & python @Arguments 2>&1
    $exitCode = $LASTEXITCODE
    Pop-Location
  } finally {
    $ErrorActionPreference = $previousErrorActionPreference
    foreach ($key in $EnvironmentOverrides.Keys) {
      [Environment]::SetEnvironmentVariable($key, $previousValues[$key], 'Process')
    }
  }

  $outputText = if ($rawOutput -is [System.Array]) {
    ($rawOutput | ForEach-Object { [string]$_ }) -join [Environment]::NewLine
  } elseif ($null -eq $rawOutput) {
    ''
  } else {
    [string]$rawOutput
  }

  return [pscustomobject]@{
    ExitCode = $exitCode
    Output = $outputText
  }
}

function Parse-WorkspaceEvents {
  param([string]$OutputText)

  $events = @()
  $prefix = "QJC_WORKSPACE_EVENT`t"
  foreach ($line in ($OutputText -split "`r?`n")) {
    if (-not $line.StartsWith($prefix)) {
      continue
    }
    $jsonText = $line.Substring($prefix.Length)
    $events += ($jsonText | ConvertFrom-Json)
  }
  return $events
}

function Invoke-WorkspaceEntry {
  param(
    [string]$ProjectRoot,
    [string]$Prompt,
    [string]$SessionId = '',
    [hashtable]$EnvironmentOverrides = @{}
  )

  $arguments = @(
    '-X', 'utf8',
    (Join-Path $ProjectRoot 'workspace_entry.py'),
    '--prompt', $Prompt
  )
  if ($ProjectRoot) {
    $arguments += @('--project-root', $ProjectRoot)
  }
  if ($SessionId) {
    $arguments += @('--session-id', $SessionId)
  }

  $result = Invoke-PythonCommand -Arguments $arguments -WorkingDirectory $ProjectRoot -EnvironmentOverrides $EnvironmentOverrides
  $events = Parse-WorkspaceEvents -OutputText $result.Output
  Assert-Condition ($events.Count -gt 0) "Workspace entry emitted no structured events. output=$($result.Output)"

  $terminalEvent = $events[-1]
  $terminalType = [string]$terminalEvent.type
  Assert-Condition (($terminalType -eq 'completed') -or ($terminalType -eq 'error')) "Workspace entry did not emit a terminal completed/error event. output=$($result.Output)"

  return [pscustomobject]@{
    ExitCode = $result.ExitCode
    Output = $result.Output
    Events = $events
    TerminalType = $terminalType
    TerminalEvent = $terminalEvent
  }
}

function Write-DouyinCoordinatorSmokeStub {
  param(
    [string]$ProjectRoot
  )

  $stubPath = Join-Path $ProjectRoot 'skills\douyin-master\scripts\coordinator.py'
  $stub = @'
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
from datetime import datetime, timezone
from pathlib import Path


def now_iso() -> str:
    return datetime.now(timezone.utc).astimezone().isoformat()


def write_json(path: Path, payload: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")


def write_text(path: Path, content: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(content, encoding="utf-8")


def update_latest(output_root: Path, project_dir: Path, summary: dict, preview_paths: dict | None = None) -> None:
    (output_root / "_latest_project_path.txt").write_text(str(project_dir), encoding="utf-8")
    write_json(output_root / "_latest_workflow_summary.json", summary)
    if preview_paths is not None:
        write_json(output_root / "_latest_preview_paths.json", preview_paths)


def main() -> int:
    parser = argparse.ArgumentParser(add_help=False)
    parser.add_argument("--product", default="")
    parser.add_argument("--audience", default="")
    parser.add_argument("--type", dest="video_type", default="")
    parser.add_argument("--style", default="")
    parser.add_argument("--output-dir", default="")
    parser.add_argument("--continue-from", default="")
    parser.add_argument("--video-engine", default="auto")
    parser.add_argument("--duration", default="")
    parser.add_argument("--seedance-duration", type=int, default=None)
    args, _ = parser.parse_known_args()

    output_root = Path(args.output_dir).resolve()
    project_dir = Path(args.continue_from).resolve() if args.continue_from else (output_root / (args.product or "douyin-project"))
    project_dir.mkdir(parents=True, exist_ok=True)

    copywriter_text_file = project_dir / "01_文案脚本.txt"
    storyboard_text_file = project_dir / "02_分镜对照.txt"
    storyboard_markdown_file = project_dir / "storyboard.md"
    video_request_file = project_dir / "video_request.json"

    selected_video_engine = "seedance" if args.video_engine in {"", "auto"} else args.video_engine
    duration_text = args.duration or (f"{args.seedance_duration}s" if args.seedance_duration else "15s")

    write_text(copywriter_text_file, f"topic: {project_dir.name}\naudience: {args.audience or 'tourist'}\nduration: {duration_text}\n")
    write_text(storyboard_text_file, "shot1: intro\nshot2: body\nshot3: outro\n")
    write_text(storyboard_markdown_file, "# Storyboard\n\n1. intro\n2. body\n3. outro\n")

    video_request = {
        "product": project_dir.name,
        "audience": args.audience or "tourist",
        "selected_video_engine": selected_video_engine,
        "copywriter_text_file": str(copywriter_text_file),
        "storyboard_text_file": str(storyboard_text_file),
        "storyboard_markdown_file": str(storyboard_markdown_file),
        "seedance_duration": args.seedance_duration or 15,
    }
    write_json(video_request_file, video_request)
    preview_paths = {
        "stage": "preview_ready",
        "project_dir": str(project_dir),
        "copywriter_text_file": str(copywriter_text_file),
        "storyboard_text_file": str(storyboard_text_file),
        "video_request_file": str(video_request_file),
    }

    if not args.continue_from:
        summary = {
            "generated_at": now_iso(),
            "stage": "preview_ready",
            "project_dir": str(project_dir),
            "config": {
                "video_engine": selected_video_engine,
                "effective_video_engine": selected_video_engine,
                "output_dir": str(project_dir),
            },
            "results": {
                "video_request": video_request,
            },
        }
        write_json(project_dir / "workflow_summary.json", summary)
        update_latest(output_root, project_dir, summary, preview_paths)
        return 0

    run_dir = project_dir / "seedance-split-runs" / "smoke-run"
    run_dir.mkdir(parents=True, exist_ok=True)
    final_video_path = run_dir / "final_video.mp4"
    latest_project_video = project_dir / "latest_seedance_split.mp4"
    final_video_path.write_bytes(b"FAKE_MP4")
    latest_project_video.write_bytes(b"FAKE_MP4")
    split_summary = {
        "project_dir": str(project_dir),
        "run_dir": str(run_dir),
        "status": "success",
        "final_video_path": str(final_video_path),
        "latest_project_video": str(latest_project_video),
        "duration_seconds": args.seedance_duration or 15,
        "segment_count": 3,
    }
    write_json(project_dir / "latest_seedance_split_summary.json", split_summary)
    write_json(
        project_dir / "video_generation_status.json",
        {
            "generated_at": now_iso(),
            "status": "success",
            "project_dir": str(project_dir),
            "video_engine": selected_video_engine,
            "current_engine": selected_video_engine,
            "estimated_wait": "",
            "message": "video generated",
        },
    )
    summary = {
        "generated_at": now_iso(),
        "stage": "video_generated",
        "project_dir": str(project_dir),
        "config": {
            "video_engine": selected_video_engine,
            "effective_video_engine": selected_video_engine,
            "output_dir": str(project_dir),
        },
        "results": {
            "video_request": video_request,
            "video_seedance": {
                "engine": "seedance",
                "mode": "split",
                "status": "success",
                "output_dir": str(run_dir),
                "manifest_path": str(project_dir / "latest_seedance_split_summary.json"),
                "video_path": str(latest_project_video),
                "video_url": "",
                "failure": "",
            },
        },
    }
    write_json(project_dir / "workflow_summary.json", summary)
    update_latest(output_root, project_dir, summary, preview_paths)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
'@
  Write-Utf8File -FilePath $stubPath -Content $stub
}

function Invoke-JsonHarness {
  param(
    [string]$HarnessPath,
    [string[]]$Arguments,
    [string]$WorkingDirectory,
    [hashtable]$EnvironmentOverrides = @{}
  )

  $commandArguments = @('-X', 'utf8', $HarnessPath) + $Arguments
  $result = Invoke-PythonCommand -Arguments $commandArguments -WorkingDirectory $WorkingDirectory -EnvironmentOverrides $EnvironmentOverrides
  Assert-Condition ($result.ExitCode -eq 0) "Harness failed: $($result.Output)"
  try {
    return $result.Output | ConvertFrom-Json
  } catch {
    throw "Harness did not emit JSON. output=$($result.Output)"
  }
}

function Run-XhsSmoke {
  param(
    [string]$RepoRoot,
    [string]$TempRoot
  )

  $xhsTopic = Decode-Utf8Base64 '5LiK5rW36L+q5aOr5bC85peF5ri45pS755Wl'
  $xhsTitle = Decode-Utf8Base64 '5LiK5rW36L+q5aOr5bC85LiA5pel5ri45pS755Wl'
  $xhsTag = Decode-Utf8Base64 '5LiK5rW36L+q5aOr5bC8'
  $xhsLocatePrompt = Decode-Utf8Base64 '55Sf5oiQ55qE56yU6K6w5Zyo5ZOq5Liq55uu5b2V5LiL5ZGi'
  $xhsLocateNote = Decode-Utf8Base64 '5paH5qGI6K6w5b2V77yaQzpcdG1wXHhoc19ub3RlLmpzb24='
  $xhsLocateMarkdown = Decode-Utf8Base64 'TWFya2Rvd24g5paH5qGj77yaQzpcdG1wXHhoc19ub3RlLm1k'
  $xhsLocateText = Decode-Utf8Base64 '5paH5pys5paH5qGj77yaQzpcdG1wXHhoc19ub3RlLnR4dA=='
  $xhsLocateImageDir = Decode-Utf8Base64 '5Zu+54mH55uu5b2V77yaQzpcdG1wXGdlbmVyYXRlZF9pbWFnZXM='
  $xhsLocateTopic = Decode-Utf8Base64 '5pyA5paw5Li76aKY77ya5LiK5rW36L+q5aOr5bC85peF5ri45pS755Wl'
  $xhsGeneratePrompt = Decode-Utf8Base64 '57uZ5oiR55Sf5oiQ5LiA5Liq5LiK5rW36L+q5aOr5bC855qE5peF5ri45pS755Wl'
  $xhsExpectedTopic = Decode-Utf8Base64 '5LiK5rW36L+q5aOr5bC855qE5peF5ri45pS755Wl'
  $xhsRetryExpectedTitle = Decode-Utf8Base64 '5aSn5a2m55Sf5omL5py65o6o6I2Q5oCO5LmI6YCJ'
  $xhsAnalysisFallbackLog = Decode-Utf8Base64 'W+WIhuaekF0g5b2T5YmN5qih5Z6L5LiN5pSv5oyB5Zu+54mH6L6T5YWl77yM5pS55Li657qv5paH5pys5YiG5p6Q'
  $xhsRetryTopicBase64 = '5aSn5a2m55Sf5omL5py65o6o6I2Q'
  $xhsRetryTitleBase64 = '5aSn5a2m55Sf5omL5py65o6o6I2Q5oCO5LmI6YCJ'
  $xhsRetryDraftBase64 = '5YWI6K+057uT6K6677yM5aSn5a2m55Sf5Lmw5omL5py65pyA5oCV5LiA5q2l5Yiw5L2N5Y+Y5oiQ5LiA5q2l5Yiw5Z2R44CC6aKE566X5Zyo5Lik5Y2D5Yiw5LiJ5Y2D5LqU5LmL6Ze077yM5LyY5YWI55yL57ut6Iiq44CB5bGP5bmV44CB5ouN54Wn5ZKM5pel5bi45rWB55WF5bqm77yM5LiN6KaB6KKr57q46Z2i5Y+C5pWw5bim5YGP44CC5aaC5p6c5L2g5bmz5pe25Li76KaB5piv5LiK6K++6K6w56yU6K6w44CB5Yi36KeG6aKR44CB5ouN5pel5bi477yM5aSE55CG5Zmo5aSf56iz44CB57O757uf5bm/5ZGK5bCR44CB5YWF55S16YCf5bqm5b+r77yM5q+U5p6B6ZmQ6LeR5YiG5pu06YeN6KaB44CC6aKE566X5pu057Sn5byg5Y+v5Lul5LyY5YWI6YCJ5aSn55S15rGg5ZKM6ICQ55So5py65Z6L77yb5aaC5p6c5L2g5Zac5qyi5ouN54Wn5ZKM56S+5Lqk5YiG5Lqr77yM5bCx5oqK5Li75pGE5ZKM5aSc5pmv6KGo546w5b6A5YmN5o6S44CC5pyA5ZCO5LiA5a6a6KaB5Y6757q/5LiL5pG455yf5py677yM55yL5omL5oSf44CB5Lqu5bqm5ZKM5Y+R54Ot77yM5YaN5Yaz5a6a54mI5pys5ZKM5a656YeP44CC'
  $xhsRetryTagOneBase64 = '5qCH562+MQ=='
  $xhsRetryTagTwoBase64 = '6auY5oCn5Lu35q+U5omL5py6'
  $xhsRetryTagThreeBase64 = '5omL5py66YG/5Z2R'
  $xhsRetryTagFourBase64 = '5pWw56CB6YCJ6LSt'
  $xhsRetryTagFiveBase64 = '5byA5a2m6KOF5aSH'
  $xhsPublishPrompt = Decode-Utf8Base64 '546w5Zyo5Y+R5biD'
  $xhsPublishError = Decode-Utf8Base64 '5bey5om+5Yiw5pyA5paw5paH5qGI77yM5L2G5b2T5YmN5rKh5pyJ5Y+v5Y+R5biD55qE5Zu+54mH'
  $xhsPublishHint = Decode-Utf8Base64 '5pyA6L+R5LiA5qyh6YWN5Zu+5o+Q56S677yaTm8gdXNhYmxlIGltYWdlIGdlbmVyYXRpb24gcHJvdmlkZXIgaXMgY29uZmlndXJlZC4='

  $sourceRoot = Join-Path $RepoRoot 'workspace\xhs'
  $packageRoot = Join-Path $TempRoot 'package'
  Copy-ProjectPackageSource -SourceRoot $sourceRoot -DestinationRoot $packageRoot

  $memoryRoot = Join-Path $packageRoot 'memory'
  $runsRoot = Join-Path $memoryRoot 'openclaw_runs'
  $generatedRoot = Join-Path $memoryRoot 'generated_images'
  New-Item -ItemType Directory -Force -Path $runsRoot, $generatedRoot | Out-Null

  $latestNote = [ordered]@{
    saved_at = '2026-04-13T18:00:00'
    topic = $xhsTopic
    title = $xhsTitle
    draft = 'body'
    tags = @($xhsTag)
    images = @()
    image_count = 0
    image_status = 'error'
    image_errors = @('No usable image generation provider is configured.')
    references_file = 'C:\tmp\xhs_run.json'
    run_record_path = 'C:\tmp\xhs_run.json'
    generated_images_dir = 'C:\tmp\generated_images'
    note_record_path = 'C:\tmp\xhs_note.json'
    note_markdown_path = 'C:\tmp\xhs_note.md'
    note_text_path = 'C:\tmp\xhs_note.txt'
  }
  Write-Utf8File -FilePath (Join-Path $runsRoot 'xhs_last_note.json') -Content (($latestNote | ConvertTo-Json -Depth 8))

  $locateResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $xhsLocatePrompt
  Assert-Condition ($locateResult.TerminalType -eq 'completed') "XHS locate smoke did not complete. output=$($locateResult.Output)"
  $locateContent = [string]$locateResult.TerminalEvent.content
  Assert-Condition ($locateContent.Contains($xhsLocateNote)) 'XHS locate smoke did not surface the note record path.'
  Assert-Condition ($locateContent.Contains($xhsLocateMarkdown)) 'XHS locate smoke did not surface the markdown note path.'
  Assert-Condition ($locateContent.Contains($xhsLocateText)) 'XHS locate smoke did not surface the text note path.'
  Assert-Condition ($locateContent.Contains($xhsLocateImageDir)) 'XHS locate smoke did not surface the image directory.'
  Assert-Condition ($locateContent.Contains($xhsLocateTopic)) 'XHS locate smoke did not preserve the latest topic.'

  $publishResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $xhsPublishPrompt
  Assert-Condition ($publishResult.TerminalType -eq 'error') "XHS publish smoke should have failed without images. output=$($publishResult.Output)"
  $publishMessage = [string]$publishResult.TerminalEvent.message
  Assert-Condition ($publishMessage.Contains($xhsPublishError)) 'XHS publish smoke did not surface the no-images publish error.'
  Assert-Condition ($publishMessage.Contains($xhsPublishHint)) 'XHS publish smoke did not surface the latest image error.'

  $configPath = Join-Path $TempRoot 'fixtures\xhs-openclaw.runtime.json'
  $xhsImageProviderConfig = Get-XhsVolcesImageProviderConfig
  $xhsTextProviderConfig = Get-XhsTextProviderFixtureConfig
  $configPayload = [ordered]@{
    models = [ordered]@{
      providers = [ordered]@{
        qwen = $xhsTextProviderConfig.providers.qwen
        modelstudio = $xhsTextProviderConfig.providers.modelstudio
        'openclaw-cloud' = $xhsTextProviderConfig.providers.'openclaw-cloud'
        volces = $xhsImageProviderConfig.providers.volces
      }
    }
    agents = [ordered]@{
      defaults = [ordered]@{
        model = [ordered]@{
          primary = $xhsTextProviderConfig.primaryModel
        }
      }
    }
  }
  Write-Utf8File -FilePath $configPath -Content (($configPayload | ConvertTo-Json -Depth 10))

  $harnessPath = Join-Path $TempRoot 'fixtures\xhs-provider-diagnostic.py'
  Write-Utf8File -FilePath $harnessPath -Content @'
import json
import os
import sys
import types

project_root = sys.argv[1]
config_path = sys.argv[2]

os.environ["OPENCLAW_CONFIG_PATH"] = config_path
sys.path.insert(0, os.path.join(project_root, "scripts"))

browser = types.ModuleType("browser")
browser.BrowserController = object
sys.modules["browser"] = browser

requests = types.ModuleType("requests")
def _blocked(*args, **kwargs):
    raise RuntimeError("requests should not be called during smoke diagnostics")
requests.post = _blocked
requests.get = _blocked
sys.modules["requests"] = requests

import run

payload = {
    "provider": run._IMAGE_PROVIDER_NAME,
    "model": run.IMAGE_MODEL,
    "api_url": run.IMAGE_API_URL,
}
try:
    run.ensure_image_api_config()
    payload["ok"] = True
except Exception as exc:
    payload["ok"] = False
    payload["error"] = str(exc)

print(json.dumps(payload, ensure_ascii=False))
'@

  $diagnostic = Invoke-JsonHarness -HarnessPath $harnessPath -Arguments @($packageRoot, $configPath) -WorkingDirectory $packageRoot
  Assert-Condition ([bool]$diagnostic.ok) 'XHS provider diagnostic smoke did not resolve the injected image provider.'
  Assert-Condition ([string]$diagnostic.provider -eq $xhsImageProviderConfig.providerName) 'XHS provider diagnostic smoke did not pick the configured provider.'
  Assert-Condition ([string]$diagnostic.model -eq $xhsImageProviderConfig.modelId) 'XHS provider diagnostic smoke did not pick the configured model.'
  Assert-Condition ([string]$diagnostic.api_url -eq 'https://ark.cn-beijing.volces.com/api/v3/images/generations') 'XHS provider diagnostic smoke produced an unexpected image API URL.'

  $textProviderHarnessPath = Join-Path $TempRoot 'fixtures\xhs-text-provider.py'
  Write-Utf8File -FilePath $textProviderHarnessPath -Content @'
import json
import os
import sys

project_root = sys.argv[1]
config_path = sys.argv[2]

os.environ["OPENCLAW_CONFIG_PATH"] = config_path
sys.path.insert(0, os.path.join(project_root, "scripts"))

import qwen

print(json.dumps({
    "provider": qwen._QWEN_PROVIDER_NAME,
    "text_model": qwen.ARK_TEXT_MODEL,
    "vision_model": qwen.ARK_VISION_MODEL,
}, ensure_ascii=False))
'@
  $textProviderResult = Invoke-JsonHarness -HarnessPath $textProviderHarnessPath -Arguments @($packageRoot, $configPath) -WorkingDirectory $packageRoot
  Assert-Condition ([string]$textProviderResult.provider -eq $xhsTextProviderConfig.providerName) 'XHS text-provider smoke did not prefer the configured local text provider over openclaw-cloud.'
  Assert-Condition ([string]$textProviderResult.text_model -eq $xhsTextProviderConfig.modelId) 'XHS text-provider smoke did not pick qwen3.5-plus as text model.'

  $topicHarnessPath = Join-Path $TempRoot 'fixtures\xhs-topic-extract.py'
  Write-Utf8File -FilePath $topicHarnessPath -Content @'
import json
import sys

project_root = sys.argv[1]
prompt = sys.argv[2]
sys.path.insert(0, project_root)

import workspace_entry

print(json.dumps({"topic": workspace_entry.extract_topic(prompt)}, ensure_ascii=False))
'@
  $topicResult = Invoke-JsonHarness -HarnessPath $topicHarnessPath -Arguments @($packageRoot, $xhsGeneratePrompt) -WorkingDirectory $packageRoot
  Assert-Condition ([string]$topicResult.topic -eq $xhsExpectedTopic) 'XHS topic extraction smoke did not strip the colloquial prefix.'

  $envProviderHarnessPath = Join-Path $TempRoot 'fixtures\xhs-env-provider.py'
  Write-Utf8File -FilePath $envProviderHarnessPath -Content @'
import json
import os
import sys
import types

project_root = sys.argv[1]
sys.path.insert(0, os.path.join(project_root, "scripts"))

browser = types.ModuleType("browser")
browser.BrowserController = object
sys.modules["browser"] = browser

requests = types.ModuleType("requests")
def _blocked(*args, **kwargs):
    raise RuntimeError("requests should not be called during smoke diagnostics")
requests.post = _blocked
requests.get = _blocked
sys.modules["requests"] = requests

import run
import qwen

run.ensure_image_api_config()

print(json.dumps({
    "text_base_url": qwen.ARK_BASE_URL,
    "text_model": qwen.ARK_TEXT_MODEL,
    "image_provider": run._IMAGE_PROVIDER_NAME,
    "image_base_url": run.IMAGE_API_BASE_URL,
    "image_model": run.IMAGE_MODEL,
}, ensure_ascii=False))
'@
  $envProviderResult = Invoke-JsonHarness -HarnessPath $envProviderHarnessPath -Arguments @($packageRoot) -WorkingDirectory $packageRoot -EnvironmentOverrides @{
    QWEN_BASE_URL = [string]$xhsTextProviderConfig.providers.qwen.baseUrl
    QWEN_API_KEY = 'smoke-key'
    QWEN_MODEL = [string]$xhsTextProviderConfig.modelId
    XHS_IMAGE_PROVIDER = [string]$xhsImageProviderConfig.providerName
    XHS_IMAGE_BASE_URL = [string]$xhsImageProviderConfig.providers.volces.baseUrl
    XHS_IMAGE_API_KEY = 'smoke-key'
    XHS_IMAGE_MODEL = [string]$xhsImageProviderConfig.modelId
  }
  Assert-Condition ([string]$envProviderResult.text_base_url -eq [string]$xhsTextProviderConfig.providers.qwen.baseUrl) 'XHS env-provider smoke did not wire QWEN_BASE_URL.'
  Assert-Condition ([string]$envProviderResult.text_model -eq $xhsTextProviderConfig.modelId) 'XHS env-provider smoke did not wire QWEN_MODEL.'
  Assert-Condition ([string]$envProviderResult.image_provider -eq $xhsImageProviderConfig.providerName) 'XHS env-provider smoke did not prefer the direct image env provider.'
  Assert-Condition ([string]$envProviderResult.image_base_url -eq [string]$xhsImageProviderConfig.providers.volces.baseUrl) 'XHS env-provider smoke did not wire XHS_IMAGE_BASE_URL.'
  Assert-Condition ([string]$envProviderResult.image_model -eq $xhsImageProviderConfig.modelId) 'XHS env-provider smoke did not wire XHS_IMAGE_MODEL.'

  $noteRetryHarnessPath = Join-Path $TempRoot 'fixtures\xhs-note-retry.py'
  Write-Utf8File -FilePath $noteRetryHarnessPath -Content @'
import base64
import contextlib
import io
import json
import os
import sys
import types

project_root = sys.argv[1]
sys.path.insert(0, os.path.join(project_root, "scripts"))

browser = types.ModuleType("browser")
browser.BrowserController = object
sys.modules["browser"] = browser

requests = types.ModuleType("requests")
def _blocked(*args, **kwargs):
    raise RuntimeError("requests should not be called during smoke note retry")
requests.post = _blocked
requests.get = _blocked
sys.modules["requests"] = requests

import run

def dec(value):
    return base64.b64decode(value).decode("utf-8")

responses = [
    json.dumps({"title": "...", "draft": "...", "tags": [dec(sys.argv[5])]}, ensure_ascii=False),
    json.dumps({
        "title": dec(sys.argv[3]),
        "draft": dec(sys.argv[4]),
        "tags": [dec(sys.argv[2]), dec(sys.argv[6]), dec(sys.argv[7]), dec(sys.argv[8]), dec(sys.argv[9])]
    }, ensure_ascii=False),
]
state = {"calls": 0}

def fake_call(messages, **kwargs):
    idx = state["calls"]
    state["calls"] += 1
    return responses[min(idx, len(responses) - 1)]

run._call_qwen = fake_call
buffer = io.StringIO()
with contextlib.redirect_stdout(buffer):
    result = run.generate_note(dec(sys.argv[2]), analysis={})

print(json.dumps({
    "calls": state["calls"],
    "title": result.get("title", ""),
    "draft_length": len(result.get("draft", "")),
    "tags_count": len(result.get("tags", [])),
}, ensure_ascii=False))
'@
  $noteRetryResult = Invoke-JsonHarness -HarnessPath $noteRetryHarnessPath -Arguments @(
    $packageRoot,
    $xhsRetryTopicBase64,
    $xhsRetryTitleBase64,
    $xhsRetryDraftBase64,
    $xhsRetryTagOneBase64,
    $xhsRetryTagTwoBase64,
    $xhsRetryTagThreeBase64,
    $xhsRetryTagFourBase64,
    $xhsRetryTagFiveBase64
  ) -WorkingDirectory $packageRoot
  Assert-Condition ([int]$noteRetryResult.calls -eq 2) 'XHS note retry smoke did not retry after placeholder content.'
  Assert-Condition ([string]$noteRetryResult.title -eq $xhsRetryExpectedTitle) 'XHS note retry smoke did not return the retried real title.'
  Assert-Condition ([int]$noteRetryResult.draft_length -ge 60) 'XHS note retry smoke did not return a usable draft.'
  Assert-Condition ([int]$noteRetryResult.tags_count -ge 3) 'XHS note retry smoke did not return enough tags.'

  $noteExportHarnessPath = Join-Path $TempRoot 'fixtures\xhs-note-export.py'
  Write-Utf8File -FilePath $noteExportHarnessPath -Content @'
import json
import os
import sys
from pathlib import Path

project_root = Path(sys.argv[1])
sys.path.insert(0, str(project_root))

import workspace_entry

workspace_entry.ensure_runtime_dirs()
payload = {
    "title": "smoke title",
    "draft": """first line
second line""",
    "tags": ["tag-a", "tag-b", "tag-c"],
    "images": [str(project_root / "memory" / "generated_images" / "img1.jpg")],
    "image_status": "success",
    "image_errors": [],
    "generated_images_dir": str(project_root / "memory" / "generated_images"),
    "notes_file": str(project_root / "memory" / "openclaw_runs" / "refs.json"),
    "analysis": {"写作风格": "real"},
}
run_record_path = project_root / "memory" / "openclaw_runs" / "xhs_test.json"
record = workspace_entry.save_latest_note(payload, topic="smoke topic", run_record_path=run_record_path)
markdown_path = Path(record["note_markdown_path"])
text_path = Path(record["note_text_path"])
print(json.dumps({
    "markdown_exists": markdown_path.exists(),
    "text_exists": text_path.exists(),
    "markdown_path": str(markdown_path),
    "text_path": str(text_path),
    "markdown_has_title": "# smoke title" in markdown_path.read_text(encoding="utf-8"),
    "text_has_draft": "first line" in text_path.read_text(encoding="utf-8"),
    "latest_has_markdown_path": json.loads(workspace_entry.LATEST_NOTE_PATH.read_text(encoding="utf-8")).get("note_markdown_path", ""),
    "latest_has_text_path": json.loads(workspace_entry.LATEST_NOTE_PATH.read_text(encoding="utf-8")).get("note_text_path", ""),
}, ensure_ascii=False))
'@
  $noteExportResult = Invoke-JsonHarness -HarnessPath $noteExportHarnessPath -Arguments @($packageRoot) -WorkingDirectory $packageRoot
  Assert-Condition ([bool]$noteExportResult.markdown_exists) 'XHS note export smoke did not create the markdown note.'
  Assert-Condition ([bool]$noteExportResult.text_exists) 'XHS note export smoke did not create the text note.'
  Assert-Condition ([bool]$noteExportResult.markdown_has_title) 'XHS note export smoke did not render markdown content.'
  Assert-Condition ([bool]$noteExportResult.text_has_draft) 'XHS note export smoke did not render text content.'
  Assert-Condition ([string]$noteExportResult.latest_has_markdown_path -eq [string]$noteExportResult.markdown_path) 'XHS note export smoke did not persist markdown path to latest note.'
  Assert-Condition ([string]$noteExportResult.latest_has_text_path -eq [string]$noteExportResult.text_path) 'XHS note export smoke did not persist text path to latest note.'
  $runScriptText = Get-Content -Path (Join-Path $packageRoot 'scripts\run.py') -Raw -Encoding utf8
  Assert-Condition ($runScriptText.Contains($xhsAnalysisFallbackLog)) 'XHS local package smoke did not preserve the readable pure-text analysis fallback log.'

  return [ordered]@{
    ok = $true
    projectId = 'xhs'
    packageRoot = $packageRoot
    locateContent = $locateContent
    publishMessage = $publishMessage
    providerName = [string]$diagnostic.provider
    providerModel = [string]$diagnostic.model
    textProviderName = [string]$textProviderResult.provider
    textProviderModel = [string]$textProviderResult.text_model
    extractedTopic = [string]$topicResult.topic
    envImageProvider = [string]$envProviderResult.image_provider
    envImageModel = [string]$envProviderResult.image_model
    noteRetryCalls = [int]$noteRetryResult.calls
    noteMarkdownPath = [string]$noteExportResult.markdown_path
    noteTextPath = [string]$noteExportResult.text_path
  }
}

function Run-DouyinSmoke {
  param(
    [string]$RepoRoot,
    [string]$TempRoot
  )

  $firstPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5oqW6Z+z5a6j5Lyg54yV54y25qGD55qE6KeG6aKR77yM57qv55S76Z2i5bCx6KGM44CC5YiG6ZWc5aS05bCR5LiA54K577yM6KeG6aKR5bCP5LiA54K5'
  $askAudience = Decode-Utf8Base64 '6L+Y5beu6Z2i5ZCR5Lq6576k'
  $askStyle = Decode-Utf8Base64 '6L+Y5beu55S76Z2i6aOO5qC8'
  $askTopic = Decode-Utf8Base64 'MS4g6L+Z5qyh6KaB5YGa55qE5Li76aKY5oiW5Lqn5ZOB5piv5LuA5LmI'
  $askVideoType = Decode-Utf8Base64 'My4g5L2g5YWI5piO56Gu6YCJ5LiA56eN'
  $secondPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5oqW6Z+z5a6j5Lyg54yV54y05qGD55qE6KeG6aKR77yM6Z2i5ZCR5ri45a6i5Lq6576k77yM57qv55S76Z2iK+aXgeeZve+8jOeyvuiHtOW5v+WRiuaEn++8jOe7meaKlumfs+mTvuaOpSBodHRwczovL3d3dy5kb3V5aW4uY29tL3ZpZGVvLzc2MDI1ODAzNzUyOTk2OTY3NTM='
  $projectDirLabel = Decode-Utf8Base64 '6aG555uu55uu5b2V77ya'
  $numberedPrompt = Decode-Utf8Base64 'M+a4uOWuouS6uue+pCAy57qv55S76Z2iK+aXgeeZvSA057K+6Ie05bm/5ZGK5oSfIOe7meaKlumfs+mTvuaOpSBodHRwczovL3d3dy5kb3V5aW4uY29tL3ZpZGVvLzc2MDI1ODAzNzUyOTk2OTY3NTM='
  $fullySpecifiedPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5pmv54K55a6j5Lyg55qE5oqW6Z+z6KeG6aKR77yM57uZ5ri45a6i5Lq6576k55yL77yM57K+6Ie05bm/5ZGK5oSf77yM57qv55S76Z2i5Yqg5peB55m977yM57uZ5oqW6Z+z6ZO+5o6lIGh0dHBzOi8vd3d3LmRvdXlpbi5jb20vdmlkZW8vNzYwMjU4MDM3NTI5OTY5Njc1M++8jOWIhumVnDPkuKrku6XlhoXvvIzop4bpopExNeenku+8jOWPquaYr+S4uuS6hua1i+ivlQ=='
  $generateVideoPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5o6o6ZSA54yV54y05qGD55qE6KeG6aKR77yM57uZ5ri45a6i5Lq6576k55yL77yM57qv55S76Z2i57K+6Ie05bm/5ZGK5oSf77yM57uZ5oqW6Z+z6ZO+5o6lIGh0dHBzOi8vd3d3LmRvdXlpbi5jb20vdmlkZW8vNzYwMjU4MDM3NTI5OTY5Njc1M++8jDE156eS77yM55Sf5oiQ6KeG6aKR'
  $videoGeneratedStage = 'video_generated'
  $finalVideoMarker = 'latest_seedance_split.mp4'

  $sourceRoot = Join-Path $RepoRoot 'workspace\douyin'
  $packageRoot = Join-Path $TempRoot 'package'
  $attachmentFixtureRoot = Join-Path $TempRoot 'fixtures'
  $attachmentImagePath = Join-Path $attachmentFixtureRoot 'digital-human.jpg'
  $attachmentAudioPath = Join-Path $attachmentFixtureRoot 'voice-reference.mp3'
  Copy-ProjectPackageSource -SourceRoot $sourceRoot -DestinationRoot $packageRoot
  Write-DouyinCoordinatorSmokeStub -ProjectRoot $packageRoot
  Write-Utf8File -FilePath $attachmentImagePath -Content 'fake image fixture'
  Write-Utf8File -FilePath $attachmentAudioPath -Content 'fake mp3 fixture'

  $sessionOne = 'local-smoke-session-1'
  $firstResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $firstPrompt -SessionId $sessionOne
  Assert-Condition ($firstResult.TerminalType -eq 'completed') "Douyin first preview smoke did not complete. output=$($firstResult.Output)"
  $firstContent = [string]$firstResult.TerminalEvent.content
  Assert-Condition ($firstContent.Contains($askAudience)) 'Douyin first preview smoke did not ask for audience.'
  Assert-Condition ($firstContent.Contains($askStyle)) 'Douyin first preview smoke did not ask for style.'
  Assert-Condition (-not $firstContent.Contains($askTopic)) 'Douyin first preview smoke still asked for topic.'
  Assert-Condition (-not $firstContent.Contains($askVideoType)) 'Douyin first preview smoke still asked for video type.'

  $secondResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $secondPrompt -SessionId $sessionOne
  Assert-Condition ($secondResult.TerminalType -eq 'completed') "Douyin second preview smoke did not complete. output=$($secondResult.Output)"
  $secondContent = [string]$secondResult.TerminalEvent.content
  Assert-Condition ($secondContent.Contains($projectDirLabel)) 'Douyin second preview smoke did not surface the project directory.'

  $pendingRoot = Join-Path $packageRoot 'memory\output\pending_intake'
  $pendingFilesAfterSecond = @(Get-ChildItem -LiteralPath $pendingRoot -File -ErrorAction SilentlyContinue)
  Assert-Condition ($pendingFilesAfterSecond.Count -eq 0) 'Douyin second preview smoke did not clear the pending intake state.'

  $sessionTwo = 'local-smoke-session-2'
  [void](Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $firstPrompt -SessionId $sessionTwo)
  $numberedResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $numberedPrompt -SessionId $sessionTwo
  Assert-Condition ($numberedResult.TerminalType -eq 'completed') "Douyin numbered preview smoke did not complete. output=$($numberedResult.Output)"
  $numberedContent = [string]$numberedResult.TerminalEvent.content
  Assert-Condition ($numberedContent.Contains($projectDirLabel)) 'Douyin numbered preview smoke did not surface the project directory.'

  $sessionThree = 'local-smoke-session-3'
  $fullPromptResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $fullySpecifiedPrompt -SessionId $sessionThree
  Assert-Condition ($fullPromptResult.TerminalType -eq 'completed') "Douyin fully specified preview smoke did not complete. output=$($fullPromptResult.Output)"
  $fullPromptContent = [string]$fullPromptResult.TerminalEvent.content
  Assert-Condition (-not $fullPromptContent.Contains($askVideoType)) 'Douyin fully specified preview smoke still asked for video type.'
  Assert-Condition ($fullPromptContent.Contains($projectDirLabel)) 'Douyin fully specified preview smoke did not surface the project directory.'
  $fullPromptLatestProjectPath = Get-Content -LiteralPath (Join-Path $packageRoot 'output\_latest_project_path.txt') -Raw -Encoding utf8
  Assert-Condition (Test-Path (Join-Path $fullPromptLatestProjectPath.Trim() 'video_request.json')) 'Douyin fully specified preview smoke did not write the video request file.'
  Assert-Condition (Test-Path (Join-Path $fullPromptLatestProjectPath.Trim() 'storyboard.md')) 'Douyin fully specified preview smoke did not write the storyboard markdown file.'

  $sessionFour = 'local-smoke-session-4'
  [void](Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $generateVideoPrompt -SessionId $sessionFour)
  $attachmentPayload = @(
    @{
      kind = 'image'
      name = 'digital-human.jpg'
      mimeType = 'image/jpeg'
      localPath = $attachmentImagePath
      projectPath = $attachmentImagePath
    },
    @{
      kind = 'file'
      name = 'voice-reference.mp3'
      mimeType = 'audio/mpeg'
      localPath = $attachmentAudioPath
      projectPath = $attachmentAudioPath
    }
  )
  $generateVideoResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt ' ' -SessionId $sessionFour -EnvironmentOverrides @{
    QJC_PROJECT_ATTACHMENTS_JSON = (ConvertTo-Json -InputObject @($attachmentPayload) -Depth 5 -Compress)
  }
  Assert-Condition ($generateVideoResult.TerminalType -eq 'completed') "Douyin generate-video prompt smoke did not complete. output=$($generateVideoResult.Output)"
  $generateVideoContent = [string]$generateVideoResult.TerminalEvent.content
  Assert-Condition (-not $generateVideoContent.Contains($askVideoType)) 'Douyin generate-video prompt smoke still asked for video type.'
  Assert-Condition ($generateVideoContent.Contains($projectDirLabel)) 'Douyin generate-video prompt smoke did not surface the project directory.'
  $latestWorkflowSummary = Get-Content -LiteralPath (Join-Path $packageRoot 'output\_latest_workflow_summary.json') -Raw -Encoding utf8 | ConvertFrom-Json
  Assert-Condition ([string]$latestWorkflowSummary.stage -eq $videoGeneratedStage) 'Douyin generate-video prompt smoke did not complete the final video stage.'
  $latestProjectPath = Get-Content -LiteralPath (Join-Path $packageRoot 'output\_latest_project_path.txt') -Raw -Encoding utf8
  $latestProjectLeaf = Split-Path -Leaf $latestProjectPath.Trim()
  $latestProjectVideo = Join-Path $latestProjectPath.Trim() 'latest_seedance_split.mp4'
  Assert-Condition ($latestProjectVideo.Contains($finalVideoMarker)) 'Douyin generate-video prompt smoke did not resolve the final video marker.'
  Assert-Condition (Test-Path $latestProjectVideo) "Douyin generate-video prompt smoke did not create the final project video. path=$latestProjectVideo"

  return [ordered]@{
    ok = $true
    projectId = 'douyin'
    packageRoot = $packageRoot
    firstContent = $firstContent
    secondContent = $secondContent
    numberedContent = $numberedContent
    fullPromptContent = $fullPromptContent
    generateVideoContent = $generateVideoContent
    generateVideoProjectLeaf = $latestProjectLeaf
    generateVideoPath = $latestProjectVideo
  }
}

$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$tempRoot = Join-Path $repoRoot '.tmp\local-project-package-smoke'
$resolvedResultPath = if ($SmokeOutput) { [System.IO.Path]::GetFullPath($SmokeOutput) } else { Join-Path $tempRoot 'result.json' }

if (Test-Path $tempRoot) {
  Remove-Item -LiteralPath $tempRoot -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null

$summary = [ordered]@{
  ok = $true
  projectId = $ProjectId
  executedAt = [DateTime]::UtcNow.ToString('o')
  results = [ordered]@{}
}

try {
  if ($ProjectId -in @('all', 'xhs')) {
    $summary.results.xhs = Run-XhsSmoke -RepoRoot $repoRoot -TempRoot (Join-Path $tempRoot 'xhs')
  }
  if ($ProjectId -in @('all', 'douyin')) {
    $summary.results.douyin = Run-DouyinSmoke -RepoRoot $repoRoot -TempRoot (Join-Path $tempRoot 'douyin')
  }

  Write-Utf8File -FilePath $resolvedResultPath -Content (($summary | ConvertTo-Json -Depth 8))
  Write-Host (($summary | ConvertTo-Json -Depth 8))
} catch {
  $failure = [ordered]@{
    ok = $false
    projectId = $ProjectId
    error = $_.Exception.Message
  }
  Write-Utf8File -FilePath $resolvedResultPath -Content (($failure | ConvertTo-Json -Depth 8))
  throw
}
