Commit bfda0c3a authored by AI-甘富林's avatar AI-甘富林

test(smoke): cover expert attachments and bundle dependencies

parent 3434b97f
......@@ -706,7 +706,7 @@ function resolveSmokeWaitForPathsTimeoutMs(): number {
}
function resolveSmokeAttachments(): Array<{
kind: "image";
kind: "image" | "file";
name: string;
mimeType: string;
localPath: string;
......@@ -732,7 +732,7 @@ function resolveSmokeAttachments(): Array<{
mimeType?: unknown;
localPath?: unknown;
};
const kind = typed.kind === "image" ? "image" : null;
const kind = typed.kind === "image" || typed.kind === "file" ? typed.kind : null;
const localPath = typeof typed.localPath === "string" ? typed.localPath.trim() : "";
if (!kind || !localPath) {
return [];
......@@ -749,7 +749,7 @@ function resolveSmokeAttachments(): Array<{
}
}
function resolveSmokeSettingsConfig(): SaveConfigInput["expertModelConfig"] | null {
function resolveSmokeSettingsConfig(): Pick<SaveConfigInput, "expertModelConfig" | "douyinRuntimeConfig"> | null {
const raw = process.env.QJCLAW_SMOKE_SETTINGS_CONFIG_JSON ?? "";
if (!raw.trim()) {
return null;
......@@ -765,6 +765,17 @@ function resolveSmokeSettingsConfig(): SaveConfigInput["expertModelConfig"] | nu
image?: { baseUrl?: unknown; apiKey?: unknown; modelId?: unknown };
video?: { baseUrl?: unknown; apiKey?: unknown; modelId?: unknown };
copywriting?: { baseUrl?: unknown; apiKey?: unknown; modelId?: unknown };
digitalHuman?: {
volcAccessKey?: unknown;
volcSecretKey?: unknown;
qiniuAccessKey?: unknown;
qiniuSecretKey?: unknown;
};
douyinRuntimeConfig?: {
videoAnalyzer?: { baseUrl?: unknown; apiKey?: unknown; modelId?: unknown };
replicationBrief?: { baseUrl?: unknown; apiKey?: unknown; modelId?: unknown };
vectcut?: { baseUrl?: unknown; fileBaseUrl?: unknown; apiKey?: unknown };
};
};
type SmokeSettingsEntry = {
......@@ -772,6 +783,22 @@ function resolveSmokeSettingsConfig(): SaveConfigInput["expertModelConfig"] | nu
apiKey?: string;
modelId?: string;
};
type SmokeDouyinTextSettingsEntry = {
baseUrl?: string;
apiKey?: string;
modelId?: string;
};
type SmokeVectCutSettingsEntry = {
baseUrl?: string;
fileBaseUrl?: string;
apiKey?: string;
};
type SmokeDigitalHumanSettingsEntry = {
volcAccessKey?: string;
volcSecretKey?: string;
qiniuAccessKey?: string;
qiniuSecretKey?: string;
};
const normalizeEntry = (entry?: { baseUrl?: unknown; apiKey?: unknown; modelId?: unknown }) => {
if (!entry || typeof entry !== "object") {
......@@ -792,14 +819,87 @@ function resolveSmokeSettingsConfig(): SaveConfigInput["expertModelConfig"] | nu
}
return Object.keys(normalized).length ? normalized : undefined;
};
const normalizeDouyinTextEntry = (entry?: { baseUrl?: unknown; apiKey?: unknown; modelId?: unknown }) => {
if (!entry || typeof entry !== "object") {
return undefined;
}
const normalized: SmokeDouyinTextSettingsEntry = {};
if (typeof entry.baseUrl === "string") {
normalized.baseUrl = entry.baseUrl;
}
if (typeof entry.apiKey === "string") {
normalized.apiKey = entry.apiKey;
}
if (typeof entry.modelId === "string") {
normalized.modelId = entry.modelId;
}
return Object.keys(normalized).length ? normalized : undefined;
};
const normalizeVectCutEntry = (entry?: { baseUrl?: unknown; fileBaseUrl?: unknown; apiKey?: unknown }) => {
if (!entry || typeof entry !== "object") {
return undefined;
}
const normalized: SmokeVectCutSettingsEntry = {};
if (typeof entry.baseUrl === "string") {
normalized.baseUrl = entry.baseUrl;
}
if (typeof entry.fileBaseUrl === "string") {
normalized.fileBaseUrl = entry.fileBaseUrl;
}
if (typeof entry.apiKey === "string") {
normalized.apiKey = entry.apiKey;
}
return Object.keys(normalized).length ? normalized : undefined;
};
const normalizeDigitalHumanEntry = (entry?: {
volcAccessKey?: unknown;
volcSecretKey?: unknown;
qiniuAccessKey?: unknown;
qiniuSecretKey?: unknown;
}) => {
if (!entry || typeof entry !== "object") {
return undefined;
}
const normalized: SmokeDigitalHumanSettingsEntry = {};
if (typeof entry.volcAccessKey === "string") {
normalized.volcAccessKey = entry.volcAccessKey;
}
if (typeof entry.volcSecretKey === "string") {
normalized.volcSecretKey = entry.volcSecretKey;
}
if (typeof entry.qiniuAccessKey === "string") {
normalized.qiniuAccessKey = entry.qiniuAccessKey;
}
if (typeof entry.qiniuSecretKey === "string") {
normalized.qiniuSecretKey = entry.qiniuSecretKey;
}
return Object.keys(normalized).length ? normalized : undefined;
};
const resolved = {
expertModelConfig: {
image: normalizeEntry(input.image),
video: normalizeEntry(input.video),
copywriting: normalizeEntry(input.copywriting)
copywriting: normalizeEntry(input.copywriting),
digitalHuman: normalizeDigitalHumanEntry(input.digitalHuman)
},
douyinRuntimeConfig: {
videoAnalyzer: normalizeDouyinTextEntry(input.douyinRuntimeConfig?.videoAnalyzer),
replicationBrief: normalizeDouyinTextEntry(input.douyinRuntimeConfig?.replicationBrief),
vectcut: normalizeVectCutEntry(input.douyinRuntimeConfig?.vectcut)
}
};
return resolved.image || resolved.video || resolved.copywriting
return resolved.expertModelConfig.image
|| resolved.expertModelConfig.video
|| resolved.expertModelConfig.copywriting
|| resolved.expertModelConfig.digitalHuman
|| resolved.douyinRuntimeConfig.videoAnalyzer
|| resolved.douyinRuntimeConfig.replicationBrief
|| resolved.douyinRuntimeConfig.vectcut
? resolved
: null;
} catch {
......@@ -1188,6 +1288,7 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
throw new Error("Settings view did not become ready for smoke validation.");
};
const defaultSmokeSettingsConfig = {
expertModelConfig: {
image: {
baseUrl: "https://image-smoke.example.com/v1",
apiKey: "image-smoke-key"
......@@ -1201,23 +1302,33 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
apiKey: "copy-smoke-key",
modelId: "qwen3.5-plus"
}
}
};
const smokeSettingsConfig = requestedSmokeSettingsConfig || defaultSmokeSettingsConfig;
const smokeExpertSettingsConfig = smokeSettingsConfig
const smokeExpertSettingsConfig = smokeSettingsConfig?.expertModelConfig
? {
image: smokeSettingsConfig.expertModelConfig.image,
video: smokeSettingsConfig.expertModelConfig.video,
copywriting: smokeSettingsConfig.expertModelConfig.copywriting,
digitalHuman: smokeSettingsConfig.expertModelConfig.digitalHuman
}
: null;
const smokeDouyinSettingsConfig = smokeSettingsConfig?.douyinRuntimeConfig
? {
image: smokeSettingsConfig.image,
video: smokeSettingsConfig.video,
copywriting: smokeSettingsConfig.copywriting
videoAnalyzer: smokeSettingsConfig.douyinRuntimeConfig.videoAnalyzer,
replicationBrief: smokeSettingsConfig.douyinRuntimeConfig.replicationBrief,
vectcut: smokeSettingsConfig.douyinRuntimeConfig.vectcut
}
: null;
const applySmokeSettingsConfig = async () => {
if (!smokeExpertSettingsConfig) {
if (!smokeExpertSettingsConfig && !smokeDouyinSettingsConfig) {
return null;
}
await actions.navigateToView("settings");
await waitForSettingsViewReady();
return await actions.saveSettingsConfig({
expertModelConfig: smokeExpertSettingsConfig
expertModelConfig: smokeExpertSettingsConfig ?? undefined,
douyinRuntimeConfig: smokeDouyinSettingsConfig ?? undefined
});
};
const runtimeCloudStatus = await api.runtimeCloud.getStatus();
......@@ -1278,7 +1389,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const followup = await actions.sendConversationPrompt(${JSON.stringify(prompt)}, {
mode: activated.viewMode,
projectId: activated.viewMode === "experts" ? activated.currentProjectId : undefined,
skillId: selectedSkillId || undefined
skillId: selectedSkillId || undefined,
attachments: smokeAttachments.length ? smokeAttachments : undefined
});
return {
...followup,
......
......@@ -7,11 +7,13 @@
- `electron-smoke.ps1` launches the desktop app directly under Electron with isolated `userData` and `logs` paths, then validates execution-policy smoke output; it now also supports preparing a workspace-entry fixture, preserving `userData`, and remote bundle-specific assertions
- `materialize-runtime-payload.ps1` generates a local bundled runtime payload under `vendor/openclaw-runtime/` by copying the local `node.exe`, the installed OpenClaw package, a local OpenClaw config snapshot, and a self-contained Python runtime with the locked dependency set installed into it; before the payload is finalized it now strips low-risk non-runtime artifacts such as non-template OpenClaw docs/README files, Python build helpers, `__pycache__`, `.pyc` / `.pyo`, and source maps, while preserving `openclaw/package/docs/reference/templates` required by OpenClaw workspace bootstrap, and writes payload size/file-count telemetry into `runtime-manifest.json`; when the existing payload manifest's `materializationKey` still matches the current inputs, it short-circuits and reuses the payload without rerunning `pip` upgrade or dependency installation
- `materialize-runtime-cache-smoke.ps1` materializes an isolated runtime directory twice and asserts the first run is a cache miss while the second run is a cache hit that skips `pip` upgrade and locked dependency installation; `pnpm smoke:materialize-cache`
- `ffmpeg-runtime-smoke.ps1` compiles the targeted `ffmpeg-runtime-smoke.ts` service-level smoke with the local desktop TypeScript toolchain, runs it under Node, and verifies bundled `ffmpeg` / `ffprobe` path resolution plus `FFMPEG_BIN` / `FFPROBE_BIN` injection into workspace automation subprocesses; `pnpm smoke:ffmpeg-runtime`
- `bundled-runtime-smoke.ps1` materializes the local runtime payload, forces bundled-runtime mode, and validates that Electron can launch and use the managed runtime end to end
- `workspace-entry-smoke.ps1` materializes the bundled runtime payload, prepares an isolated active project fixture, and validates the workspace-entry execution path end to end as a formal regression smoke; `pnpm smoke:workspace-entry`
- `cloud-bundle-smoke.ps1` generates real same-project bundle variants, serves them through the smoke cloud API, and validates the full `cloud zip -> bundle sync -> active project -> workspace-entry` chain for payload `sync`, cached `init`, and same-`projectId` replacement with refreshed README/shared-entry materialization; `pnpm smoke:cloud-bundle`
- `xhs-expert-cloud-bundle-smoke.ps1` packages `workspace/xhs` as a zip-backed employee-config bundle, injects the fixed `volces` image provider into the managed runtime config for XHS generation, preserves two extra fixture experts so the experts rail exceeds two items, switches to the XHS expert, and sends `发一个美食推荐类的帖子` through the experts view; `pnpm smoke:xhs-expert-cloud-bundle`
- `douyin-expert-cloud-bundle-smoke.ps1` packages `workspace/douyin` as a zip-backed employee-config bundle, preserves two extra fixture experts so the experts rail exceeds two items, switches to the Douyin expert, and sends `帮我做一个关于防晒喷雾的抖音视频文案` through the experts view; `pnpm smoke:douyin-expert-cloud-bundle`
- `douyin-expert-pdf-attachment-smoke.ps1` generates a local PDF fixture, runs the Douyin experts-page smoke with that PDF through the chat attachment button path, and validates that the attachment materializes under `inputs/assets/manual`; `pnpm smoke:douyin-expert-pdf-attachment`
- `local-project-package-smoke.ps1` copies the current local `workspace/xhs` and `workspace/douyin` sources with `bundlePackaging.excludePaths` applied, runs package-level workspace-entry smoke checks from the copied package roots, verifies the XHS path/publish behavior, verifies injected XHS image-provider resolution plus topic extraction cleanup, and verifies the Douyin multi-turn intake flow; `pnpm smoke:local-project-package`
- `xhs-expert-manual-launch.ps1` packages `workspace/xhs` into a local zip bundle, boots the packaged desktop app against the built-in mock `/openclaw-employee-config`, injects the fixed `volces` image provider plus `XHS_IMAGE_PROVIDER/XHS_IMAGE_MODEL`, preserves two extra fixture experts so the experts rail exceeds two items, and leaves the app open for manual experts-page testing; close any already running `千匠问天.exe` instance first, then run `powershell -ExecutionPolicy Bypass -File build/scripts/xhs-expert-manual-launch.ps1`
- `douyin-expert-manual-launch.ps1` packages `workspace/douyin` into a local zip bundle with `bundlePackaging.excludePaths` applied, boots the packaged desktop app against the built-in mock `/openclaw-employee-config`, preserves two extra fixture experts so the experts rail exceeds two items, and leaves the app open for manual experts-page testing; close any already running `千匠问天.exe` instance first, then run `powershell -ExecutionPolicy Bypass -File build/scripts/douyin-expert-manual-launch.ps1`
......
......@@ -4,6 +4,8 @@ param(
[int]$SmokePort = 4318,
[string]$SmokeToken = 'smoke-token',
[string]$BaseOutputDir,
[string]$SettingsConfigPath,
[string]$AttachmentFixturePath,
[int]$TimeoutSeconds = 180,
[switch]$SkipMaterializeRuntime
)
......@@ -29,6 +31,34 @@ function Write-Base64File {
[System.IO.File]::WriteAllBytes($FilePath, [System.Convert]::FromBase64String($Base64))
}
function Get-AttachmentMimeType {
param(
[string]$FilePath
)
switch ([System.IO.Path]::GetExtension($FilePath).ToLowerInvariant()) {
'.png' { return 'image/png' }
'.jpg' { return 'image/jpeg' }
'.jpeg' { return 'image/jpeg' }
'.webp' { return 'image/webp' }
'.gif' { return 'image/gif' }
'.bmp' { return 'image/bmp' }
'.pdf' { return 'application/pdf' }
'.ppt' { return 'application/vnd.ms-powerpoint' }
'.pptx' { return 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }
'.xls' { return 'application/vnd.ms-excel' }
'.xlsx' { return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
'.csv' { return 'text/csv' }
'.tsv' { return 'text/tab-separated-values' }
'.doc' { return 'application/msword' }
'.docx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }
'.txt' { return 'text/plain' }
'.md' { return 'text/markdown' }
'.json' { return 'application/json' }
default { return 'application/octet-stream' }
}
}
function Copy-ProjectBundleSource {
param(
[string]$SourceRoot,
......@@ -56,6 +86,9 @@ function Copy-ProjectBundleSource {
if ($excludeSet.Contains($entry.Name)) {
continue
}
if ($entry.Name -eq '.tmp-test-runtime') {
continue
}
if ($entry.PSIsContainer -and $entry.Name -match '^tmp[a-z0-9]{6,}$') {
Write-Warning "Skipping transient temp directory from bundle source: $($entry.FullName)"
continue
......@@ -119,6 +152,297 @@ function New-ExpertFixtureProject {
Write-Utf8File (Join-Path $projectRoot 'AGENTS.md') "# $ProjectName`n`nThis is a passive fixture expert used for desktop UI smoke coverage."
}
function Invoke-DouyinRuntimeSelfChecks {
param(
[string]$RepoRoot,
[string]$ProjectRoot,
[bool]$RequireVectCut = $true
)
$projectEnvPath = Join-Path $ProjectRoot 'memory\project.env'
if (-not (Test-Path $projectEnvPath)) {
throw "Douyin runtime self-checks require an existing project.env: $projectEnvPath"
}
$projectRootEnv = Join-Path $ProjectRoot '.env'
if (Test-Path $projectRootEnv) {
throw "Douyin runtime self-checks expected no project root .env, but found: $projectRootEnv"
}
$runtimePythonExe = Join-Path $RepoRoot 'vendor\openclaw-runtime\python\python.exe'
if (-not (Test-Path $runtimePythonExe)) {
throw "Bundled runtime python was not found: $runtimePythonExe"
}
$checkScripts = @(
'skills\video-llm-analyzer\scripts\check_setup.py',
'skills\replication-brief-builder\scripts\check_setup.py',
'skills\seedance-2-0-video-generator\scripts\check_setup.py',
'skills\jimeng-omnihuman\scripts\check_setup.py'
)
if ($RequireVectCut) {
$checkScripts += 'skills\douyin-post-editor\scripts\check_setup.py'
}
$previousClientFlag = $env:QJC_CLIENT_CONFIG_ACTIVE
$env:QJC_CLIENT_CONFIG_ACTIVE = '1'
try {
$results = @()
foreach ($relativeScript in $checkScripts) {
$scriptPath = Join-Path $ProjectRoot $relativeScript
if (-not (Test-Path $scriptPath)) {
throw "Douyin runtime self-check script was not found: $scriptPath"
}
$outputLines = @(& $runtimePythonExe $scriptPath 2>&1)
$exitCode = $LASTEXITCODE
$outputText = (($outputLines | ForEach-Object { [string]$_ }) -join "`n").Trim()
if ($exitCode -ne 0) {
throw "Douyin runtime self-check failed for $relativeScript`n$outputText"
}
$jsonStart = $outputText.IndexOf('{')
$jsonEnd = $outputText.LastIndexOf('}')
if ($jsonStart -lt 0 -or $jsonEnd -lt $jsonStart) {
throw "Douyin runtime self-check did not emit JSON for $relativeScript`n$outputText"
}
$reportText = $outputText.Substring($jsonStart, $jsonEnd - $jsonStart + 1)
$report = $reportText | ConvertFrom-Json
$results += [ordered]@{
script = $relativeScript.Replace('\', '/')
configSource = [string]$report.config_source
runtimeEnvExists = [bool]$report.runtime_env_exists
clientConfigActive = [bool]$report.client_config_active
}
}
return @($results)
}
finally {
if ($null -eq $previousClientFlag) {
Remove-Item Env:QJC_CLIENT_CONFIG_ACTIVE -ErrorAction SilentlyContinue
} else {
$env:QJC_CLIENT_CONFIG_ACTIVE = $previousClientFlag
}
}
}
function Get-ConfigValueFromSection {
param(
[string]$SectionText,
[string]$Key
)
if (-not $SectionText) {
return ""
}
$pattern = "(?im)^\s*$([regex]::Escape($Key))\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$"
$match = [regex]::Match($SectionText, $pattern)
if (-not $match.Success) {
return ""
}
return $match.Groups[1].Value.Trim()
}
function Convert-RealDouyinSettingsText {
param(
[string]$Text
)
$normalizedText = ($Text -replace "`r", "").Trim()
if (-not $normalizedText) {
throw "Settings config text is empty."
}
$preBriefText = $normalizedText
$replicationStart = [regex]::Match($normalizedText, '(?im)^\s*Replication Brief\s*$')
if ($replicationStart.Success) {
$preBriefText = $normalizedText.Substring(0, $replicationStart.Index)
}
$baseUrlMatches = [regex]::Matches($preBriefText, '(?im)^\s*baseUrl\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$')
$modelMatches = [regex]::Matches($preBriefText, '(?im)^\s*model\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$')
$apiKeyMatches = [regex]::Matches($preBriefText, '(?im)^\s*api_key\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$')
if ($baseUrlMatches.Count -lt 3 -or $modelMatches.Count -lt 3 -or $apiKeyMatches.Count -lt 3) {
throw "Failed to parse copywriting, video, and image settings from text config."
}
$replicationText = ""
if ($replicationStart.Success) {
$replicationText = $normalizedText.Substring($replicationStart.Index)
}
$videoAnalyzerText = ""
$videoAnalyzerStart = [regex]::Match($normalizedText, '(?im)^\s*Video Analyzer\s*$')
if ($videoAnalyzerStart.Success) {
$videoAnalyzerText = $normalizedText.Substring($videoAnalyzerStart.Index)
}
$vectcutApiKey = [regex]::Match($normalizedText, '(?im)^\s*VECTCUT_API_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim()
$vectcutBaseUrl = [regex]::Match($normalizedText, '(?im)^\s*VECTCUT_BASE_URL\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim()
$vectcutFileBaseUrl = [regex]::Match($normalizedText, '(?im)^\s*VECTCUT_FILE_BASE_URL\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim()
$config = [ordered]@{
copywriting = [ordered]@{
baseUrl = $baseUrlMatches[0].Groups[1].Value.Trim()
apiKey = $apiKeyMatches[0].Groups[1].Value.Trim()
modelId = $modelMatches[0].Groups[1].Value.Trim()
}
video = [ordered]@{
baseUrl = $baseUrlMatches[1].Groups[1].Value.Trim()
apiKey = $apiKeyMatches[1].Groups[1].Value.Trim()
modelId = $modelMatches[1].Groups[1].Value.Trim()
}
image = [ordered]@{
baseUrl = $baseUrlMatches[2].Groups[1].Value.Trim()
apiKey = $apiKeyMatches[2].Groups[1].Value.Trim()
modelId = $modelMatches[2].Groups[1].Value.Trim()
}
digitalHuman = [ordered]@{
volcAccessKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_VOLC_ACCESS_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
volcSecretKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_VOLC_SECRET_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
qiniuAccessKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_QINIU_ACCESS_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
qiniuSecretKey = ([regex]::Match($normalizedText, '(?im)^\s*OMNIHUMAN_QINIU_SECRET_KEY\s*(?:[:=]|\uFF1A)\s*(.+?)\s*$').Groups[1].Value.Trim())
}
douyinRuntimeConfig = [ordered]@{
videoAnalyzer = [ordered]@{
baseUrl = (Get-ConfigValueFromSection -SectionText $videoAnalyzerText -Key 'ARK_BASE_URL')
apiKey = (Get-ConfigValueFromSection -SectionText $videoAnalyzerText -Key 'ARK_API_KEY')
modelId = (Get-ConfigValueFromSection -SectionText $videoAnalyzerText -Key 'VIDEO_LLM_ANALYZER_MODEL')
}
replicationBrief = [ordered]@{
baseUrl = (Get-ConfigValueFromSection -SectionText $replicationText -Key 'base_url')
apiKey = (Get-ConfigValueFromSection -SectionText $replicationText -Key 'api_key')
modelId = (Get-ConfigValueFromSection -SectionText $replicationText -Key 'model')
}
}
}
if ($vectcutApiKey -or $vectcutBaseUrl -or $vectcutFileBaseUrl) {
$config.douyinRuntimeConfig.vectcut = [ordered]@{}
if ($vectcutBaseUrl) {
$config.douyinRuntimeConfig.vectcut.baseUrl = $vectcutBaseUrl
}
if ($vectcutFileBaseUrl) {
$config.douyinRuntimeConfig.vectcut.fileBaseUrl = $vectcutFileBaseUrl
}
if ($vectcutApiKey) {
$config.douyinRuntimeConfig.vectcut.apiKey = $vectcutApiKey
}
}
return $config
}
function Resolve-SmokeSettingsConfig {
param(
[string]$PathValue
)
$defaultConfig = [ordered]@{
image = [ordered]@{
baseUrl = 'https://ark.cn-beijing.volces.com/api/v3/images/generations'
apiKey = 'image-smoke-key'
modelId = 'doubao-seedream-5-0-260128'
}
video = [ordered]@{
baseUrl = 'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks'
apiKey = 'video-smoke-key'
modelId = 'seedance-1-0-pro-250528'
}
copywriting = [ordered]@{
baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
apiKey = 'copy-smoke-key'
modelId = 'qwen3.5-plus'
}
digitalHuman = [ordered]@{
volcAccessKey = 'digital-human-volc-ak'
volcSecretKey = 'digital-human-volc-sk'
qiniuAccessKey = 'digital-human-qiniu-ak'
qiniuSecretKey = 'digital-human-qiniu-sk'
}
douyinRuntimeConfig = [ordered]@{
videoAnalyzer = [ordered]@{
baseUrl = 'https://ark.cn-beijing.volces.com/api/v3'
apiKey = 'video-analyzer-smoke-key'
modelId = 'video-analyzer-smoke-model'
}
replicationBrief = [ordered]@{
baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
apiKey = 'replication-brief-smoke-key'
modelId = 'replication-brief-smoke-model'
}
vectcut = [ordered]@{
baseUrl = 'https://open.vectcut.com/cut_jianying'
fileBaseUrl = 'https://open.vectcut.com'
apiKey = 'vectcut-smoke-key'
}
}
}
if (-not $PathValue) {
return $defaultConfig
}
$resolvedPath = (Resolve-Path $PathValue).Path
$raw = Get-Content -LiteralPath $resolvedPath -Raw -Encoding UTF8
if (-not $raw.Trim()) {
throw "Settings config file is empty: $resolvedPath"
}
try {
$parsedJson = $raw | ConvertFrom-Json -Depth 20 -AsHashtable
if ($parsedJson) {
return $parsedJson
}
}
catch {
}
return (Convert-RealDouyinSettingsText -Text $raw)
}
function Seed-SmokeSecrets {
param(
[string]$UserDataPath,
[hashtable]$SmokeSettingsConfig
)
$secretFilePath = Join-Path $UserDataPath 'config\secrets.dev.json'
$payload = [ordered]@{
note = "Development fallback only. Replace this file-based secret store with keytar before shipping."
}
$seededSecretCount = 0
$digitalHuman = $SmokeSettingsConfig.digitalHuman
if ($digitalHuman) {
if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.volcAccessKey)) {
$payload.digitalHumanVolcAccessKey = [string]$digitalHuman.volcAccessKey
$seededSecretCount += 1
}
if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.volcSecretKey)) {
$payload.digitalHumanVolcSecretKey = [string]$digitalHuman.volcSecretKey
$seededSecretCount += 1
}
if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.qiniuAccessKey)) {
$payload.digitalHumanQiniuAccessKey = [string]$digitalHuman.qiniuAccessKey
$seededSecretCount += 1
}
if (-not [string]::IsNullOrWhiteSpace([string]$digitalHuman.qiniuSecretKey)) {
$payload.digitalHumanQiniuSecretKey = [string]$digitalHuman.qiniuSecretKey
$seededSecretCount += 1
}
}
if ($seededSecretCount -eq 0) {
return
}
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $secretFilePath) | Out-Null
Write-Utf8File $secretFilePath ($payload | ConvertTo-Json -Depth 6)
}
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if (-not $BaseOutputDir) {
$BaseOutputDir = Join-Path $repoRoot '.tmp\douyin-expert-cloud-bundle-smoke'
......@@ -138,28 +462,22 @@ $expectedBundleSourceUrl = "http://127.0.0.1:$SmokePort/downloads/$bundleFileNam
$expertPrompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('5biu5oiR5YGa5LiA5Liq5YWz5LqO6Ziy5pmS5Za36Zu+55qE5oqW6Z+z6KeG6aKR5paH5qGI'))
$expectedExpertIds = @('browser-expert-smoke', 'douyin', 'xhs-expert-smoke')
$electronSmokeScript = Join-Path $repoRoot 'build\scripts\electron-smoke.ps1'
$attachmentFixturePath = Join-Path $BaseOutputDir 'fixtures\smoke-image.png'
$expectedAttachmentDir = Join-Path $userDataPath 'projects\douyin\inputs\images\main'
$attachmentFixtureName = 'smoke-image.png'
$defaultAttachmentFixturePath = Join-Path $BaseOutputDir 'fixtures\smoke-image.png'
$attachmentFixtureResolvedPath = ''
$attachmentFixtureName = ''
$attachmentKind = 'image'
$attachmentMimeType = 'image/png'
$expectedAttachmentRelativeDir = 'inputs/images/main'
$expectedAttachmentExtension = '.png'
$attachmentPayload = @(
@{
kind = 'image'
kind = $attachmentKind
name = $attachmentFixtureName
mimeType = 'image/png'
localPath = $attachmentFixturePath
mimeType = $attachmentMimeType
localPath = $attachmentFixtureResolvedPath
}
)
$smokeSettingsConfig = [ordered]@{
image = [ordered]@{
apiKey = 'image-smoke-key'
}
video = [ordered]@{
apiKey = 'video-smoke-key'
}
copywriting = [ordered]@{
apiKey = 'copy-smoke-key'
}
}
$smokeSettingsConfig = Resolve-SmokeSettingsConfig -PathValue $SettingsConfigPath
$douyinSourceCandidates = @(
(Join-Path $repoRoot 'workspace\douyin')
)
......@@ -169,12 +487,31 @@ if (Test-Path $BaseOutputDir) {
Remove-Item $BaseOutputDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $BaseOutputDir, $bundleSourceRoot, $userDataPath, $logsPath | Out-Null
Seed-SmokeSecrets -UserDataPath $userDataPath -SmokeSettingsConfig $smokeSettingsConfig
if (-not $douyinSourceRoot) {
throw "Douyin workspace source was not found in any expected location: $($douyinSourceCandidates -join ', ')"
}
Write-Base64File -FilePath $attachmentFixturePath -Base64 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACOSURBVHhe7dAxAQAwDITA+tf4XlIDOIDhFkbetjNrAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEUT+YDdB6OSM1gxG4BEAAAAAElFTkSuQmCC'
$resolvedAttachmentSource = $AttachmentFixturePath
if (-not $resolvedAttachmentSource) {
$resolvedAttachmentSource = $defaultAttachmentFixturePath
Write-Base64File -FilePath $resolvedAttachmentSource -Base64 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACOSURBVHhe7dAxAQAwDITA+tf4XlIDOIDhFkbetjNrAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEWTBlA0aQBFkwZQNGkARZMGUDRpAEUT+YDdB6OSM1gxG4BEAAAAAElFTkSuQmCC'
}
$attachmentFixtureResolvedPath = (Resolve-Path $resolvedAttachmentSource).Path
$attachmentFixtureName = Split-Path -Leaf $attachmentFixtureResolvedPath
$expectedAttachmentExtension = [System.IO.Path]::GetExtension($attachmentFixtureResolvedPath).ToLowerInvariant()
$attachmentKind = if (@('.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp') -contains $expectedAttachmentExtension) { 'image' } else { 'file' }
$attachmentMimeType = Get-AttachmentMimeType -FilePath $attachmentFixtureResolvedPath
$expectedAttachmentRelativeDir = if ($attachmentKind -eq 'image') { 'inputs/images/main' } else { 'inputs/assets/manual' }
$attachmentPayload = @(
@{
kind = $attachmentKind
name = $attachmentFixtureName
mimeType = $attachmentMimeType
localPath = $attachmentFixtureResolvedPath
}
)
Copy-ProjectBundleSource -SourceRoot $douyinSourceRoot -DestinationRoot (Join-Path $bundleSourceRoot 'douyin')
if (Test-Path $bundleZipPath) {
Remove-Item $bundleZipPath -Force
......@@ -227,10 +564,41 @@ try {
'-TimeoutSeconds', $TimeoutSeconds
)
$vectcutConfig = $smokeSettingsConfig.douyinRuntimeConfig.vectcut
$expectedVectcutBaseUrl = if ($vectcutConfig) { [string]$vectcutConfig.baseUrl } else { '' }
$expectedVectcutFileBaseUrl = if ($vectcutConfig) { [string]$vectcutConfig.fileBaseUrl } else { '' }
$expectedVectcutApiKey = if ($vectcutConfig) { [string]$vectcutConfig.apiKey } else { '' }
$summary = & node -e @"
const fs = require('fs');
const path = require('path');
const [smokeOutput, userDataPath, expectedBundleSourceUrl, expectedBundleConfigVersion, expectedBundleFileName, expectedBundleSkillId, expectedPrompt, expectedExpertIdsCsv, expectedImageBaseUrl, expectedImageApiKey, expectedImageModelId, expectedCopyBaseUrl, expectedCopyApiKey, expectedCopyModelId] = process.argv.slice(1);
const [
smokeOutput,
userDataPath,
expectedBundleSourceUrl,
expectedBundleConfigVersion,
expectedBundleFileName,
expectedBundleSkillId,
expectedPrompt,
expectedExpertIdsCsv,
expectedAttachmentRelativeDir,
expectedAttachmentExtension,
expectedImageBaseUrl,
expectedImageApiKey,
expectedImageModelId,
expectedCopyBaseUrl,
expectedCopyApiKey,
expectedCopyModelId,
expectedVideoAnalyzerBaseUrl,
expectedVideoAnalyzerApiKey,
expectedVideoAnalyzerModelId,
expectedReplicationBriefBaseUrl,
expectedReplicationBriefApiKey,
expectedReplicationBriefModelId,
expectedVectcutBaseUrl,
expectedVectcutFileBaseUrl,
expectedVectcutApiKey
] = process.argv.slice(1);
const result = JSON.parse(fs.readFileSync(smokeOutput, 'utf8'));
if (!result.ok) {
throw new Error(result.error || 'Smoke failed.');
......@@ -274,6 +642,7 @@ const nonHomeProjects = Array.isArray(workspaceSummary.projects)
: [];
const settingsSave = sendResult.settingsSave || {};
const savedModelConfig = settingsSave.expertModelConfig || {};
const savedDouyinRuntimeConfig = settingsSave.douyinRuntimeConfig || {};
const expertProjectIds = Array.isArray(finalState.expertProjectIds)
? finalState.expertProjectIds.map((value) => String(value || '')).sort()
: [];
......@@ -315,18 +684,47 @@ if (String(sendResult.smokeProjectId || '') !== 'douyin') {
if (String(sendResult.prompt || '') !== expectedPrompt) {
throw new Error('Smoke prompt mismatch.');
}
if (String(savedModelConfig.image && savedModelConfig.image.baseUrl || '') !== 'https://ark.cn-beijing.volces.com/api/v3/images/generations') {
if (String(savedModelConfig.image && savedModelConfig.image.baseUrl || '') !== expectedImageBaseUrl) {
throw new Error('Saved image baseUrl mismatch: ' + String(savedModelConfig.image && savedModelConfig.image.baseUrl || ''));
}
if (String(savedModelConfig.image && savedModelConfig.image.modelId || '') !== 'doubao-seedream-5-0-260128') {
if (String(savedModelConfig.image && savedModelConfig.image.modelId || '') !== expectedImageModelId) {
throw new Error('Saved image modelId mismatch: ' + String(savedModelConfig.image && savedModelConfig.image.modelId || ''));
}
if (String(savedModelConfig.copywriting && savedModelConfig.copywriting.baseUrl || '') !== 'https://dashscope.aliyuncs.com/compatible-mode/v1') {
if (String(savedModelConfig.copywriting && savedModelConfig.copywriting.baseUrl || '') !== expectedCopyBaseUrl) {
throw new Error('Saved copywriting baseUrl mismatch: ' + String(savedModelConfig.copywriting && savedModelConfig.copywriting.baseUrl || ''));
}
if (String(savedModelConfig.copywriting && savedModelConfig.copywriting.modelId || '') !== 'qwen3.5-plus') {
if (String(savedModelConfig.copywriting && savedModelConfig.copywriting.modelId || '') !== expectedCopyModelId) {
throw new Error('Saved copywriting modelId mismatch: ' + String(savedModelConfig.copywriting && savedModelConfig.copywriting.modelId || ''));
}
if (String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.baseUrl || '') !== expectedVideoAnalyzerBaseUrl) {
throw new Error('Saved videoAnalyzer baseUrl mismatch: ' + String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.baseUrl || ''));
}
if (String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.modelId || '') !== expectedVideoAnalyzerModelId) {
throw new Error('Saved videoAnalyzer modelId mismatch: ' + String(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.modelId || ''));
}
if (!Boolean(savedDouyinRuntimeConfig.videoAnalyzer && savedDouyinRuntimeConfig.videoAnalyzer.apiKeyConfigured)) {
throw new Error('Saved videoAnalyzer apiKeyConfigured mismatch.');
}
if (String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.baseUrl || '') !== expectedReplicationBriefBaseUrl) {
throw new Error('Saved replicationBrief baseUrl mismatch: ' + String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.baseUrl || ''));
}
if (String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.modelId || '') !== expectedReplicationBriefModelId) {
throw new Error('Saved replicationBrief modelId mismatch: ' + String(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.modelId || ''));
}
if (!Boolean(savedDouyinRuntimeConfig.replicationBrief && savedDouyinRuntimeConfig.replicationBrief.apiKeyConfigured)) {
throw new Error('Saved replicationBrief apiKeyConfigured mismatch.');
}
if (expectedVectcutApiKey) {
if (String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.baseUrl || '') !== expectedVectcutBaseUrl) {
throw new Error('Saved vectcut baseUrl mismatch: ' + String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.baseUrl || ''));
}
if (String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.fileBaseUrl || '') !== expectedVectcutFileBaseUrl) {
throw new Error('Saved vectcut fileBaseUrl mismatch: ' + String(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.fileBaseUrl || ''));
}
if (!Boolean(savedDouyinRuntimeConfig.vectcut && savedDouyinRuntimeConfig.vectcut.apiKeyConfigured)) {
throw new Error('Saved vectcut apiKeyConfigured mismatch.');
}
}
if (String(workspaceSummary.currentProjectId || '') !== 'douyin') {
throw new Error('Final active project was not douyin: ' + String(workspaceSummary.currentProjectId || ''));
}
......@@ -361,7 +759,7 @@ if (smokeAttachments.length !== 1) {
}
const attachmentSessionId = String(sendResult.sessionId || '');
const sessionSlug = sanitizeAttachmentFileComponent(attachmentSessionId.replace(/[:]/g, '-'));
const expectedAttachmentRelativePath = 'inputs/images/main/' + sessionSlug + '-01.png';
const expectedAttachmentRelativePath = expectedAttachmentRelativeDir + '/' + sessionSlug + '-01' + expectedAttachmentExtension;
const expectedAttachmentPath = path.join(userDataPath, 'projects', 'douyin', ...expectedAttachmentRelativePath.split('/'));
if (!fs.existsSync(expectedAttachmentPath)) {
throw new Error('Materialized attachment file was not found: ' + expectedAttachmentPath);
......@@ -377,7 +775,10 @@ if (!assistantContent.includes('Project attachments:')) {
if (!assistantContent.includes(expectedAttachmentRelativePath)) {
throw new Error('Assistant content did not reference the materialized attachment path: ' + expectedAttachmentRelativePath);
}
if (String(projectEnv.DOUYIN_WRITER_LLM_BASE_URL || '') !== 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions') {
const expectedWriterBaseUrl = expectedCopyBaseUrl.endsWith('/chat/completions')
? expectedCopyBaseUrl
: expectedCopyBaseUrl.replace(/\/+$/, '') + '/chat/completions';
if (String(projectEnv.DOUYIN_WRITER_LLM_BASE_URL || '') !== expectedWriterBaseUrl) {
throw new Error('project.env DOUYIN_WRITER_LLM_BASE_URL mismatch: ' + String(projectEnv.DOUYIN_WRITER_LLM_BASE_URL || ''));
}
if (String(projectEnv.DASHSCOPE_API_KEY || '') !== expectedCopyApiKey) {
......@@ -386,18 +787,59 @@ if (String(projectEnv.DASHSCOPE_API_KEY || '') !== expectedCopyApiKey) {
if (String(projectEnv.QWEN_API_KEY || '') !== expectedCopyApiKey) {
throw new Error('project.env QWEN_API_KEY mismatch.');
}
if (String(projectEnv.DOUYIN_WRITER_LLM_MODEL || '') !== 'qwen3.5-plus') {
if (String(projectEnv.DOUYIN_WRITER_LLM_MODEL || '') !== expectedCopyModelId) {
throw new Error('project.env DOUYIN_WRITER_LLM_MODEL mismatch: ' + String(projectEnv.DOUYIN_WRITER_LLM_MODEL || ''));
}
if (String(projectEnv.SEEDREAM_ARK_BASE_URL || '') !== 'https://ark.cn-beijing.volces.com/api/v3') {
const expectedSeedreamBaseUrl = expectedImageBaseUrl.endsWith('/images/generations')
? expectedImageBaseUrl.slice(0, -'/images/generations'.length)
: expectedImageBaseUrl;
if (String(projectEnv.SEEDREAM_ARK_BASE_URL || '') !== expectedSeedreamBaseUrl) {
throw new Error('project.env SEEDREAM_ARK_BASE_URL mismatch: ' + String(projectEnv.SEEDREAM_ARK_BASE_URL || ''));
}
if (String(projectEnv.SEEDREAM_ARK_API_KEY || '') !== expectedImageApiKey) {
throw new Error('project.env SEEDREAM_ARK_API_KEY mismatch.');
}
if (String(projectEnv.SEEDREAM_MODEL || '') !== 'doubao-seedream-5-0-260128') {
if (String(projectEnv.SEEDREAM_MODEL || '') !== expectedImageModelId) {
throw new Error('project.env SEEDREAM_MODEL mismatch: ' + String(projectEnv.SEEDREAM_MODEL || ''));
}
if (String(projectEnv.QJC_CLIENT_CONFIG_ACTIVE || '') !== '1') {
throw new Error('project.env QJC_CLIENT_CONFIG_ACTIVE mismatch: ' + String(projectEnv.QJC_CLIENT_CONFIG_ACTIVE || ''));
}
if (String(projectEnv.VIDEO_LLM_ANALYZER_BASE_URL || '') !== expectedVideoAnalyzerBaseUrl) {
throw new Error('project.env VIDEO_LLM_ANALYZER_BASE_URL mismatch: ' + String(projectEnv.VIDEO_LLM_ANALYZER_BASE_URL || ''));
}
if (String(projectEnv.VIDEO_LLM_ANALYZER_API_KEY || '') !== expectedVideoAnalyzerApiKey) {
throw new Error('project.env VIDEO_LLM_ANALYZER_API_KEY mismatch.');
}
if (String(projectEnv.VIDEO_LLM_ANALYZER_MODEL || '') !== expectedVideoAnalyzerModelId) {
throw new Error('project.env VIDEO_LLM_ANALYZER_MODEL mismatch: ' + String(projectEnv.VIDEO_LLM_ANALYZER_MODEL || ''));
}
if (String(projectEnv.ARK_BASE_URL || '') !== expectedVideoAnalyzerBaseUrl) {
throw new Error('project.env ARK_BASE_URL mismatch: ' + String(projectEnv.ARK_BASE_URL || ''));
}
if (String(projectEnv.ARK_API_KEY || '') !== expectedVideoAnalyzerApiKey) {
throw new Error('project.env ARK_API_KEY mismatch.');
}
if (String(projectEnv.REPLICATION_BRIEF_BASE_URL || '') !== expectedReplicationBriefBaseUrl) {
throw new Error('project.env REPLICATION_BRIEF_BASE_URL mismatch: ' + String(projectEnv.REPLICATION_BRIEF_BASE_URL || ''));
}
if (String(projectEnv.REPLICATION_BRIEF_API_KEY || '') !== expectedReplicationBriefApiKey) {
throw new Error('project.env REPLICATION_BRIEF_API_KEY mismatch.');
}
if (String(projectEnv.REPLICATION_BRIEF_MODEL || '') !== expectedReplicationBriefModelId) {
throw new Error('project.env REPLICATION_BRIEF_MODEL mismatch: ' + String(projectEnv.REPLICATION_BRIEF_MODEL || ''));
}
if (expectedVectcutApiKey) {
if (String(projectEnv.VECTCUT_BASE_URL || '') !== expectedVectcutBaseUrl) {
throw new Error('project.env VECTCUT_BASE_URL mismatch: ' + String(projectEnv.VECTCUT_BASE_URL || ''));
}
if (String(projectEnv.VECTCUT_FILE_BASE_URL || '') !== expectedVectcutFileBaseUrl) {
throw new Error('project.env VECTCUT_FILE_BASE_URL mismatch: ' + String(projectEnv.VECTCUT_FILE_BASE_URL || ''));
}
if (String(projectEnv.VECTCUT_API_KEY || '') !== expectedVectcutApiKey) {
throw new Error('project.env VECTCUT_API_KEY mismatch.');
}
}
console.log(JSON.stringify({
ok: true,
smokeOutput,
......@@ -418,11 +860,14 @@ console.log(JSON.stringify({
statusLabels,
bundleManifestPath
}, null, 2));
"@ $smokeOutput $userDataPath $expectedBundleSourceUrl $bundleConfigVersion $bundleFileName $bundleSkillId $expertPrompt ($expectedExpertIds -join ',') $smokeSettingsConfig.image.baseUrl $smokeSettingsConfig.image.apiKey $smokeSettingsConfig.image.modelId $smokeSettingsConfig.copywriting.baseUrl $smokeSettingsConfig.copywriting.apiKey $smokeSettingsConfig.copywriting.modelId
"@ $smokeOutput $userDataPath $expectedBundleSourceUrl $bundleConfigVersion $bundleFileName $bundleSkillId $expertPrompt ($expectedExpertIds -join ',') $expectedAttachmentRelativeDir $expectedAttachmentExtension $smokeSettingsConfig.image.baseUrl $smokeSettingsConfig.image.apiKey $smokeSettingsConfig.image.modelId $smokeSettingsConfig.copywriting.baseUrl $smokeSettingsConfig.copywriting.apiKey $smokeSettingsConfig.copywriting.modelId $smokeSettingsConfig.douyinRuntimeConfig.videoAnalyzer.baseUrl $smokeSettingsConfig.douyinRuntimeConfig.videoAnalyzer.apiKey $smokeSettingsConfig.douyinRuntimeConfig.videoAnalyzer.modelId $smokeSettingsConfig.douyinRuntimeConfig.replicationBrief.baseUrl $smokeSettingsConfig.douyinRuntimeConfig.replicationBrief.apiKey $smokeSettingsConfig.douyinRuntimeConfig.replicationBrief.modelId $expectedVectcutBaseUrl $expectedVectcutFileBaseUrl $expectedVectcutApiKey
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$requireVectCut = -not [string]::IsNullOrWhiteSpace($expectedVectcutApiKey)
$runtimeSelfCheckResults = Invoke-DouyinRuntimeSelfChecks -RepoRoot $repoRoot -ProjectRoot (Join-Path $userDataPath 'projects\douyin') -RequireVectCut $requireVectCut
Write-Output $summary
Write-Output ($runtimeSelfCheckResults | ConvertTo-Json -Depth 5)
}
finally {
Remove-Item Env:QJCLAW_SMOKE_BUNDLE_ZIP_PATH -ErrorAction SilentlyContinue
......
param(
[int]$GatewayPort = 18889,
[string]$GatewayToken = 'qjc-bundled-runtime-token',
[int]$SmokePort = 4318,
[string]$SmokeToken = 'smoke-token',
[string]$BaseOutputDir,
[int]$TimeoutSeconds = 180,
[switch]$SkipMaterializeRuntime
)
$ErrorActionPreference = 'Stop'
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
function New-SmokePdfFixture {
param(
[string]$FilePath
)
$directory = Split-Path -Parent $FilePath
if ($directory) {
New-Item -ItemType Directory -Force -Path $directory | Out-Null
}
$objects = @(
'1 0 obj' + "`n" +
'<< /Type /Catalog /Pages 2 0 R >>' + "`n" +
'endobj' + "`n",
'2 0 obj' + "`n" +
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>' + "`n" +
'endobj' + "`n",
'3 0 obj' + "`n" +
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 320 160] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>' + "`n" +
'endobj' + "`n",
'4 0 obj' + "`n" +
'<< /Length 48 >>' + "`n" +
'stream' + "`n" +
'BT' + "`n" +
'/F1 18 Tf' + "`n" +
'36 96 Td' + "`n" +
'(QJClaw PDF smoke fixture) Tj' + "`n" +
'ET' + "`n" +
'endstream' + "`n" +
'endobj' + "`n",
'5 0 obj' + "`n" +
'<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>' + "`n" +
'endobj' + "`n"
)
$builder = New-Object System.Text.StringBuilder
[void]$builder.Append("%PDF-1.4`n")
$offsets = @()
foreach ($objectText in $objects) {
$offsets += [System.Text.Encoding]::ASCII.GetByteCount($builder.ToString())
[void]$builder.Append($objectText)
}
$xrefOffset = [System.Text.Encoding]::ASCII.GetByteCount($builder.ToString())
[void]$builder.Append("xref`n")
[void]$builder.Append("0 6`n")
[void]$builder.Append("0000000000 65535 f `n")
foreach ($offset in $offsets) {
[void]$builder.Append(([string]::Format('{0:0000000000} 00000 n `n', $offset)))
}
[void]$builder.Append("trailer`n")
[void]$builder.Append("<< /Size 6 /Root 1 0 R >>`n")
[void]$builder.Append("startxref`n")
[void]$builder.Append("$xrefOffset`n")
[void]$builder.Append("%%EOF`n")
[System.IO.File]::WriteAllText($FilePath, $builder.ToString(), $utf8NoBom)
}
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if (-not $BaseOutputDir) {
$BaseOutputDir = Join-Path $repoRoot '.tmp\douyin-expert-pdf-attachment-smoke'
}
$BaseOutputDir = [System.IO.Path]::GetFullPath($BaseOutputDir)
$pdfFixtureDir = Join-Path $repoRoot '.tmp\pdf-fixtures'
$pdfFixturePath = Join-Path $pdfFixtureDir 'douyin-smoke-attachment.pdf'
New-SmokePdfFixture -FilePath $pdfFixturePath
$argumentList = @(
'-ExecutionPolicy', 'Bypass',
'-File', (Join-Path $repoRoot 'build\scripts\douyin-expert-cloud-bundle-smoke.ps1'),
'-GatewayPort', $GatewayPort,
'-GatewayToken', $GatewayToken,
'-SmokePort', $SmokePort,
'-SmokeToken', $SmokeToken,
'-BaseOutputDir', $BaseOutputDir,
'-AttachmentFixturePath', $pdfFixturePath,
'-TimeoutSeconds', $TimeoutSeconds
)
if ($SkipMaterializeRuntime) {
$argumentList += '-SkipMaterializeRuntime'
}
powershell @argumentList
exit $LASTEXITCODE
......@@ -574,6 +574,17 @@ if (expectBundled === 'true') {
if (!runtimeStatus.installedPythonPackages.some((value) => { const normalized = String(value || '').toLowerCase(); return normalized === 'python-dotenv' || normalized.startsWith('python-dotenv=='); })) {
throw new Error('Bundled runtime did not report python-dotenv in the Python package set.');
}
if (!runtimeStatus.installedPythonPackages.some((value) => { const normalized = String(value || '').toLowerCase(); return normalized === 'loguru' || normalized.startsWith('loguru=='); })) {
throw new Error('Bundled runtime did not report loguru in the Python package set.');
}
if (!runtimeStatus.installedPythonPackages.some((value) => { const normalized = String(value || '').toLowerCase(); return normalized === 'pyexecjs' || normalized.startsWith('pyexecjs=='); })) {
throw new Error('Bundled runtime did not report PyExecJS in the Python package set.');
}
for (const packageName of ['certifi', 'openai', 'retry', 'fastapi', 'uvicorn', 'python-multipart', 'pdfplumber']) {
if (!runtimeStatus.installedPythonPackages.some((value) => { const normalized = String(value || '').toLowerCase(); return normalized === packageName || normalized.startsWith(packageName + '=='); })) {
throw new Error('Bundled runtime did not report ' + packageName + ' in the Python package set.');
}
}
if (!runtimeStatus.installedPythonPackages.some((value) => { const normalized = String(value || '').toLowerCase(); return normalized === 'pillow' || normalized.startsWith('pillow=='); })) {
throw new Error('Bundled runtime did not report Pillow in the Python package set.');
}
......
......@@ -997,15 +997,24 @@ import json
result = {
"ok": True,
"moduleSet": False,
"xhsModuleSet": False,
"openaiAsyncImport": False,
"greenletImport": False,
"playwrightAsyncImport": False,
"edgeTtsImport": False
}
try:
import openpyxl, pandas, requests, bs4, lxml, pypdf, docx, charset_normalizer, yaml, PIL, dotenv, playwright, edge_tts
import openpyxl, pandas, requests, bs4, lxml, urllib3, pypdf, docx, charset_normalizer, yaml, PIL, dotenv, playwright, edge_tts, certifi
result["moduleSet"] = True
import openai, retry, fastapi, uvicorn, multipart, pdfplumber
result["xhsModuleSet"] = True
from openai import AsyncOpenAI
assert AsyncOpenAI is not None
result["openaiAsyncImport"] = True
import greenlet
result["greenletImport"] = True
......@@ -1051,6 +1060,8 @@ print(json.dumps(result))
exitCode = $pythonImportProbeExitCode
output = $pythonImportProbeRaw
moduleSet = [bool]($pythonImportProbeJson -and $pythonImportProbeJson.moduleSet)
xhsModuleSet = [bool]($pythonImportProbeJson -and $pythonImportProbeJson.xhsModuleSet)
openaiAsyncImport = [bool]($pythonImportProbeJson -and $pythonImportProbeJson.openaiAsyncImport)
greenletImport = [bool]($pythonImportProbeJson -and $pythonImportProbeJson.greenletImport)
playwrightAsyncImport = [bool]($pythonImportProbeJson -and $pythonImportProbeJson.playwrightAsyncImport)
edgeTtsImport = [bool]($pythonImportProbeJson -and $pythonImportProbeJson.edgeTtsImport)
......
......@@ -390,6 +390,7 @@ def main() -> int:
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,
......@@ -434,6 +435,7 @@ def main() -> int:
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,
......@@ -862,26 +864,27 @@ function Run-DouyinSmoke {
)
$firstPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5oqW6Z+z5a6j5Lyg54yV54y25qGD55qE6KeG6aKR77yM57qv55S76Z2i5bCx6KGM44CC5YiG6ZWc5aS05bCR5LiA54K577yM6KeG6aKR5bCP5LiA54K5'
$askAudience = Decode-Utf8Base64 'Mi4g6L+Z5p2h6KeG6aKR5Li76KaB57uZ6LCB55yL'
$askStyle = Decode-Utf8Base64 'NC4g5L2g5oOz6KaB5LuA5LmI6aOO5qC8'
$askAudience = Decode-Utf8Base64 '6L+Y5beu6Z2i5ZCR5Lq6576k'
$askStyle = Decode-Utf8Base64 '6L+Y5beu55S76Z2i6aOO5qC8'
$askTopic = Decode-Utf8Base64 'MS4g6L+Z5qyh6KaB5YGa55qE5Li76aKY5oiW5Lqn5ZOB5piv5LuA5LmI'
$askVideoType = Decode-Utf8Base64 'My4g5L2g5YWI5piO56Gu6YCJ5LiA56eN'
$secondPrompt = Decode-Utf8Base64 '57uZ5ri45a6i55yL77yM5oiR5oOz6KaB57K+6Ie05bm/5ZGK5oSf'
$completedPrefix = Decode-Utf8Base64 '5bey5a6M5oiQ5oqW6Z+z6aG555uu5omn6KGM'
$secondPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5oqW6Z+z5a6j5Lyg54yV54y05qGD55qE6KeG6aKR77yM6Z2i5ZCR5ri45a6i5Lq6576k77yM57qv55S76Z2iK+aXgeeZve+8jOeyvuiHtOW5v+WRiuaEn++8jOe7meaKlumfs+mTvuaOpSBodHRwczovL3d3dy5kb3V5aW4uY29tL3ZpZGVvLzc2MDI1ODAzNzUyOTk2OTY3NTM='
$projectDirLabel = Decode-Utf8Base64 '6aG555uu55uu5b2V77ya'
$videoRequestLabel = Decode-Utf8Base64 '6KeG6aKR6K+35rGC77ya'
$storyboardMarkdownLabel = Decode-Utf8Base64 '5YiG6ZWcIE1hcmtkb3du77ya'
$numberedPrompt = Decode-Utf8Base64 'Mua4uOWuoiA057K+6Ie05bm/5ZGK5oSf'
$fullySpecifiedPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5pmv54K55a6j5Lyg55qE5oqW6Z+z6KeG6aKR77yM57uZ5ri45a6i55yL77yM57K+6Ie05bm/5ZGK5oSf77yM57qv55S76Z2i5Yqg5peB55m977yM5YiG6ZWcM+S4quS7peWGhe+8jOinhumikTE156eS77yM5Y+q5piv5Li65LqG5rWL6K+V'
$generateVideoPrompt = Decode-Utf8Base64 '5biu5oiR5YGa5LiA5Liq5o6o6ZSA54yV54y05qGD55qE6KeG6aKR77yM57uZ5ri45a6i55yL77yM57qv55S76Z2i57K+6Ie05bm/5ZGK5oSf77yMMTXnp5LvvIznlJ/miJDop4bpopE='
$kiwiTopic = Decode-Utf8Base64 '54yV54y05qGD'
$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
......@@ -895,7 +898,6 @@ function Run-DouyinSmoke {
$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($completedPrefix)) 'Douyin second preview smoke did not continue into execution.'
Assert-Condition ($secondContent.Contains($projectDirLabel)) 'Douyin second preview smoke did not surface the project directory.'
$pendingRoot = Join-Path $packageRoot 'memory\output\pending_intake'
......@@ -907,31 +909,49 @@ function Run-DouyinSmoke {
$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($completedPrefix)) 'Douyin numbered preview smoke did not complete execution.'
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 ($fullPromptContent.Contains($completedPrefix)) 'Douyin fully specified preview smoke did not execute immediately.'
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.'
Assert-Condition ($fullPromptContent.Contains($videoRequestLabel)) 'Douyin fully specified preview smoke did not surface the video request file.'
Assert-Condition ($fullPromptContent.Contains($storyboardMarkdownLabel)) 'Douyin fully specified preview smoke did not surface the storyboard markdown file.'
$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'
$generateVideoResult = Invoke-WorkspaceEntry -ProjectRoot $packageRoot -Prompt $generateVideoPrompt -SessionId $sessionFour
[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 ($generateVideoContent.Contains($completedPrefix)) 'Douyin generate-video prompt smoke did not execute immediately.'
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.'
Assert-Condition ($generateVideoContent.Contains($videoGeneratedStage)) 'Douyin generate-video prompt smoke did not complete the final video stage.'
Assert-Condition ($generateVideoContent.Contains($finalVideoMarker)) 'Douyin generate-video prompt smoke did not surface the final video path.'
$latestProjectPath = Get-Content -LiteralPath (Join-Path $packageRoot 'memory\output\_latest_project_path.txt') -Raw -Encoding utf8
$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()
Assert-Condition ($latestProjectLeaf.Contains($kiwiTopic)) "Douyin generate-video prompt smoke routed to the wrong topic directory. latest=$latestProjectLeaf"
$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]@{
......
......@@ -105,6 +105,7 @@ $logsDir = Join-Path $BaseOutputDir 'logs'
$materializeScript = Join-Path $repoRoot 'build\scripts\materialize-runtime-payload.ps1'
$manifestPath = Join-Path $runtimeDir 'runtime-manifest.json'
$pythonManifestPath = Join-Path $runtimeDir 'python\python-manifest.json'
$ffmpegPath = Join-Path $runtimeDir 'ffmpeg\bin\ffmpeg.exe'
if (Test-Path $BaseOutputDir) {
Remove-Item -LiteralPath $BaseOutputDir -Recurse -Force -ErrorAction SilentlyContinue
......@@ -138,6 +139,9 @@ if (-not (Test-Path $manifestPath)) {
if (-not (Test-Path $pythonManifestPath)) {
throw "Expected Python manifest at $pythonManifestPath after first run."
}
if (-not (Test-Path $ffmpegPath)) {
throw "Expected bundled ffmpeg at $ffmpegPath after first run."
}
$secondRun = Invoke-MaterializeRun `
-Label 'second-run' `
......
......@@ -101,6 +101,50 @@ function Copy-ProjectBundleSource {
}
}
function Install-XhsNodeDependencies {
param([string]$ProjectRoot)
$packageJsonPath = Join-Path $ProjectRoot 'package.json'
$packageLockPath = Join-Path $ProjectRoot 'package-lock.json'
if (-not (Test-Path $packageJsonPath) -or -not (Test-Path $packageLockPath)) {
throw "XHS bundle staging directory is missing package.json or package-lock.json: $ProjectRoot"
}
Write-Host "Installing XHS Node dependencies in staging directory: $ProjectRoot"
Push-Location $ProjectRoot
try {
npm ci --omit=dev --no-audit --fund=false
if ($LASTEXITCODE -ne 0) {
throw "npm ci failed while preparing XHS bundle staging directory."
}
} finally {
Pop-Location
}
}
function Assert-XhsBundleNodeDependencies {
param([string]$ZipPath)
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
try {
$entries = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($entry in $archive.Entries) {
[void]$entries.Add($entry.FullName.Replace('\', '/'))
}
foreach ($requiredEntry in @(
'xhs/node_modules/crypto-js/package.json',
'xhs/node_modules/jsdom/package.json'
)) {
if (-not $entries.Contains($requiredEntry)) {
throw "XHS bundle zip is missing required Node dependency entry: $requiredEntry"
}
}
} finally {
$archive.Dispose()
}
}
function Invoke-ElectronSmokeWithRetry {
param(
[string]$ScriptPath,
......@@ -182,13 +226,19 @@ $attachmentPayload = @(
)
$smokeSettingsConfig = [ordered]@{
image = [ordered]@{
baseUrl = 'https://ark.cn-beijing.volces.com/api/v3/images/generations'
apiKey = 'image-smoke-key'
modelId = 'doubao-seedream-5-0-260128'
}
video = [ordered]@{
baseUrl = 'https://ark.cn-beijing.volces.com/api/v3'
apiKey = 'video-smoke-key'
modelId = 'doubao-seedance-2-0-260128'
}
copywriting = [ordered]@{
baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
apiKey = 'copy-smoke-key'
modelId = 'qwen3.5-plus'
}
}
$xhsSourceCandidates = @(
......@@ -208,11 +258,14 @@ if (-not $xhsSourceRoot) {
}
Write-Base64File -FilePath $attachmentFixturePath -Base64 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aKJwAAAAASUVORK5CYII='
Copy-ProjectBundleSource -SourceRoot $xhsSourceRoot -DestinationRoot (Join-Path $bundleSourceRoot 'xhs')
$stagedXhsRoot = Join-Path $bundleSourceRoot 'xhs'
Copy-ProjectBundleSource -SourceRoot $xhsSourceRoot -DestinationRoot $stagedXhsRoot
Install-XhsNodeDependencies -ProjectRoot $stagedXhsRoot
if (Test-Path $bundleZipPath) {
Remove-Item $bundleZipPath -Force
}
Compress-Archive -Path (Join-Path $bundleSourceRoot 'xhs') -DestinationPath $bundleZipPath -Force
Assert-XhsBundleNodeDependencies -ZipPath $bundleZipPath
$projectsRoot = Join-Path $userDataPath 'projects'
$manifestsRoot = Join-Path $userDataPath 'manifests'
......
......@@ -112,6 +112,50 @@ function Copy-ProjectBundleSource {
}
}
function Install-XhsNodeDependencies {
param([string]$ProjectRoot)
$packageJsonPath = Join-Path $ProjectRoot 'package.json'
$packageLockPath = Join-Path $ProjectRoot 'package-lock.json'
if (-not (Test-Path $packageJsonPath) -or -not (Test-Path $packageLockPath)) {
throw "XHS bundle staging directory is missing package.json or package-lock.json: $ProjectRoot"
}
Write-Host "Installing XHS Node dependencies in staging directory: $ProjectRoot"
Push-Location $ProjectRoot
try {
npm ci --omit=dev --no-audit --fund=false
if ($LASTEXITCODE -ne 0) {
throw "npm ci failed while preparing XHS bundle staging directory."
}
} finally {
Pop-Location
}
}
function Assert-XhsBundleNodeDependencies {
param([string]$ZipPath)
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
try {
$entries = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($entry in $archive.Entries) {
[void]$entries.Add($entry.FullName.Replace('\', '/'))
}
foreach ($requiredEntry in @(
'xhs/node_modules/crypto-js/package.json',
'xhs/node_modules/jsdom/package.json'
)) {
if (-not $entries.Contains($requiredEntry)) {
throw "XHS bundle zip is missing required Node dependency entry: $requiredEntry"
}
}
} finally {
$archive.Dispose()
}
}
function Reset-XhsLiveRunBundleState {
param([string]$ProjectRoot)
......@@ -273,12 +317,15 @@ if (-not $xhsSourceRoot) {
}
Write-Base64File -FilePath $attachmentFixturePath -Base64 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aKJwAAAAASUVORK5CYII='
Copy-ProjectBundleSource -SourceRoot $xhsSourceRoot -DestinationRoot (Join-Path $bundleSourceRoot 'xhs')
Reset-XhsLiveRunBundleState -ProjectRoot (Join-Path $bundleSourceRoot 'xhs')
$stagedXhsRoot = Join-Path $bundleSourceRoot 'xhs'
Copy-ProjectBundleSource -SourceRoot $xhsSourceRoot -DestinationRoot $stagedXhsRoot
Install-XhsNodeDependencies -ProjectRoot $stagedXhsRoot
Reset-XhsLiveRunBundleState -ProjectRoot $stagedXhsRoot
if (Test-Path $bundleZipPath) {
Remove-Item $bundleZipPath -Force
}
Compress-Archive -Path (Join-Path $bundleSourceRoot 'xhs') -DestinationPath $bundleZipPath -Force
Assert-XhsBundleNodeDependencies -ZipPath $bundleZipPath
$projectsRoot = Join-Path $userDataPath 'projects'
$manifestsRoot = Join-Path $userDataPath 'manifests'
......
......@@ -116,6 +116,50 @@ function Copy-ProjectBundleSource {
}
}
function Install-XhsNodeDependencies {
param([string]$ProjectRoot)
$packageJsonPath = Join-Path $ProjectRoot 'package.json'
$packageLockPath = Join-Path $ProjectRoot 'package-lock.json'
if (-not (Test-Path $packageJsonPath) -or -not (Test-Path $packageLockPath)) {
throw "XHS bundle staging directory is missing package.json or package-lock.json: $ProjectRoot"
}
Write-Host "Installing XHS Node dependencies in staging directory: $ProjectRoot"
Push-Location $ProjectRoot
try {
npm ci --omit=dev --no-audit --fund=false
if ($LASTEXITCODE -ne 0) {
throw "npm ci failed while preparing XHS bundle staging directory."
}
} finally {
Pop-Location
}
}
function Assert-XhsBundleNodeDependencies {
param([string]$ZipPath)
Add-Type -AssemblyName System.IO.Compression.FileSystem
$archive = [System.IO.Compression.ZipFile]::OpenRead($ZipPath)
try {
$entries = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($entry in $archive.Entries) {
[void]$entries.Add($entry.FullName.Replace('\', '/'))
}
foreach ($requiredEntry in @(
'xhs/node_modules/crypto-js/package.json',
'xhs/node_modules/jsdom/package.json'
)) {
if (-not $entries.Contains($requiredEntry)) {
throw "XHS bundle zip is missing required Node dependency entry: $requiredEntry"
}
}
} finally {
$archive.Dispose()
}
}
function New-ExpertFixtureProject {
param(
[string]$ProjectsRoot,
......@@ -186,11 +230,14 @@ if (Test-Path $BaseOutputDir) {
New-Item -ItemType Directory -Force -Path $BaseOutputDir, $userDataPath, $logsPath, $bundleSourceRoot | Out-Null
Copy-ProjectBundleSource -SourceRoot $xhsSourceRoot -DestinationRoot (Join-Path $bundleSourceRoot 'xhs')
$stagedXhsRoot = Join-Path $bundleSourceRoot 'xhs'
Copy-ProjectBundleSource -SourceRoot $xhsSourceRoot -DestinationRoot $stagedXhsRoot
Install-XhsNodeDependencies -ProjectRoot $stagedXhsRoot
if (Test-Path $bundleZipPath) {
Remove-Item -LiteralPath $bundleZipPath -Force
}
Compress-Archive -Path (Join-Path $bundleSourceRoot 'xhs') -DestinationPath $bundleZipPath -Force
Assert-XhsBundleNodeDependencies -ZipPath $bundleZipPath
$projectsRoot = Join-Path $userDataPath 'projects'
$manifestsRoot = Join-Path $userDataPath 'manifests'
......
......@@ -18,6 +18,7 @@
"smoke:cloud-bundle": "powershell -ExecutionPolicy Bypass -File build/scripts/cloud-bundle-smoke.ps1",
"smoke:xhs-expert-cloud-bundle": "powershell -ExecutionPolicy Bypass -File build/scripts/xhs-expert-cloud-bundle-smoke.ps1",
"smoke:douyin-expert-cloud-bundle": "powershell -ExecutionPolicy Bypass -File build/scripts/douyin-expert-cloud-bundle-smoke.ps1",
"smoke:douyin-expert-pdf-attachment": "powershell -ExecutionPolicy Bypass -File build/scripts/douyin-expert-pdf-attachment-smoke.ps1",
"smoke:local-project-package": "powershell -ExecutionPolicy Bypass -File build/scripts/local-project-package-smoke.ps1",
"smoke:xhs-local-project-package": "powershell -ExecutionPolicy Bypass -File build/scripts/local-project-package-smoke.ps1 -ProjectId xhs",
"smoke:douyin-local-project-package": "powershell -ExecutionPolicy Bypass -File build/scripts/local-project-package-smoke.ps1 -ProjectId douyin",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment