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 = {
image: normalizeEntry(input.image),
video: normalizeEntry(input.video),
copywriting: normalizeEntry(input.copywriting)
expertModelConfig: {
image: normalizeEntry(input.image),
video: normalizeEntry(input.video),
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,36 +1288,47 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
throw new Error("Settings view did not become ready for smoke validation.");
};
const defaultSmokeSettingsConfig = {
image: {
baseUrl: "https://image-smoke.example.com/v1",
apiKey: "image-smoke-key"
},
video: {
baseUrl: "https://video-smoke.example.com/v1",
apiKey: "video-smoke-key"
},
copywriting: {
baseUrl: "https://copy-smoke.example.com/v1",
apiKey: "copy-smoke-key",
modelId: "qwen3.5-plus"
expertModelConfig: {
image: {
baseUrl: "https://image-smoke.example.com/v1",
apiKey: "image-smoke-key"
},
video: {
baseUrl: "https://video-smoke.example.com/v1",
apiKey: "video-smoke-key"
},
copywriting: {
baseUrl: "https://copy-smoke.example.com/v1",
apiKey: "copy-smoke-key",
modelId: "qwen3.5-plus"
}
}
};
const smokeSettingsConfig = requestedSmokeSettingsConfig || defaultSmokeSettingsConfig;
const smokeExpertSettingsConfig = smokeSettingsConfig
const smokeExpertSettingsConfig = smokeSettingsConfig?.expertModelConfig
? {
image: smokeSettingsConfig.image,
video: smokeSettingsConfig.video,
copywriting: smokeSettingsConfig.copywriting
image: smokeSettingsConfig.expertModelConfig.image,
video: smokeSettingsConfig.expertModelConfig.video,
copywriting: smokeSettingsConfig.expertModelConfig.copywriting,
digitalHuman: smokeSettingsConfig.expertModelConfig.digitalHuman
}
: null;
const smokeDouyinSettingsConfig = smokeSettingsConfig?.douyinRuntimeConfig
? {
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`
......
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)
......
......@@ -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