Commit 61fd84f1 authored by AI-甘富林's avatar AI-甘富林

feat(desktop): add settings model config smoke coverage

Add desktop settings persistence for expert model config and cover the new save flow with a dedicated settings smoke test.
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent ee9c7dd7
...@@ -83,9 +83,18 @@ interface RendererSmokeState { ...@@ -83,9 +83,18 @@ interface RendererSmokeState {
} | null; } | null;
skills: Array<{ id: string; name: string }>; skills: Array<{ id: string; name: string }>;
modelConfig: { modelConfig: {
defaultChatModelLabel?: string; image?: {
routingMode?: string; baseUrl?: string;
itemCount?: number; apiKeyConfigured?: boolean;
};
video?: {
baseUrl?: string;
apiKeyConfigured?: boolean;
};
copywriting?: {
baseUrl?: string;
apiKeyConfigured?: boolean;
};
} | null; } | null;
systemSummary: { systemSummary: {
isPackaged?: boolean; isPackaged?: boolean;
...@@ -280,6 +289,20 @@ async function waitForRendererSmokeState(window: BrowserWindow, timeoutMs = 2000 ...@@ -280,6 +289,20 @@ async function waitForRendererSmokeState(window: BrowserWindow, timeoutMs = 2000
return null; return null;
} }
function matchesExpectedSmokeModelConfig(state: RendererSmokeState | null | undefined): boolean {
const modelConfig = state?.modelConfig;
if (!modelConfig) {
return false;
}
return String(modelConfig.image?.baseUrl || "") === "https://image-smoke.example.com/v1"
&& String(modelConfig.video?.baseUrl || "") === "https://video-smoke.example.com/v1"
&& String(modelConfig.copywriting?.baseUrl || "") === "https://copy-smoke.example.com/v1"
&& Boolean(modelConfig.image?.apiKeyConfigured)
&& Boolean(modelConfig.video?.apiKeyConfigured)
&& Boolean(modelConfig.copywriting?.apiKeyConfigured);
}
async function waitForRendererSmokeBootstrap(window: BrowserWindow, timeoutMs = 20000): Promise<RendererSmokeState> { async function waitForRendererSmokeBootstrap(window: BrowserWindow, timeoutMs = 20000): Promise<RendererSmokeState> {
return await new Promise<RendererSmokeState>((resolve, reject) => { return await new Promise<RendererSmokeState>((resolve, reject) => {
let settled = false; let settled = false;
...@@ -648,6 +671,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -648,6 +671,8 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
? "experts" ? "experts"
: requestedSmokeViewMode === "skills" : requestedSmokeViewMode === "skills"
? "skills" ? "skills"
: requestedSmokeViewMode === "settings"
? "settings"
: "chat"; : "chat";
const smokeProjectId = process.env.QJCLAW_SMOKE_PROJECT_ID?.trim() || ""; const smokeProjectId = process.env.QJCLAW_SMOKE_PROJECT_ID?.trim() || "";
const smokeAttachments = resolveSmokeAttachments(); const smokeAttachments = resolveSmokeAttachments();
...@@ -771,6 +796,17 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -771,6 +796,17 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
runtimeState: workspace.runtimeState runtimeState: workspace.runtimeState
})); }));
}; };
const waitForSettingsViewReady = async () => {
const deadline = Date.now() + 20000;
while (Date.now() < deadline) {
const rendererState = window.__QJC_SMOKE__;
if (rendererState?.viewMode === "settings" && rendererState.config && rendererState.modelConfig) {
return rendererState;
}
await sleep(250);
}
throw new Error("Settings view did not become ready for smoke validation.");
};
const runtimeCloudStatus = await api.runtimeCloud.getStatus(); const runtimeCloudStatus = await api.runtimeCloud.getStatus();
let runtimeCloudFetch = runtimeCloudStatus; let runtimeCloudFetch = runtimeCloudStatus;
let runtimeCloudFetchError = null; let runtimeCloudFetchError = null;
...@@ -797,8 +833,10 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -797,8 +833,10 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const credits = session.state === "authenticated" ? await api.credits.getSummary() : null; const credits = session.state === "authenticated" ? await api.credits.getSummary() : null;
const skills = session.state === "authenticated" ? await api.skills.list() : []; const skills = session.state === "authenticated" ? await api.skills.list() : [];
const skillsPageState = smokeViewMode === "skills" ? await waitForSkillsPageReady() : null; const skillsPageState = smokeViewMode === "skills" ? await waitForSkillsPageReady() : null;
const workspace = skillsPageState?.workspace ?? await waitForWorkspaceReady(); const workspace = smokeViewMode === "settings"
const readyWorkspaceSkills = workspace.skills.filter((skill) => skill.ready); ? await api.workspace.getSummary()
: skillsPageState?.workspace ?? await waitForWorkspaceReady();
const readyWorkspaceSkills = Array.isArray(workspace.skills) ? workspace.skills.filter((skill) => skill.ready) : [];
const readySkills = skills.filter((skill) => skill.ready); const readySkills = skills.filter((skill) => skill.ready);
const selectedSkillId = preferredSkillId const selectedSkillId = preferredSkillId
? (readyWorkspaceSkills.find((skill) => skill.id === preferredSkillId)?.id ? (readyWorkspaceSkills.find((skill) => skill.id === preferredSkillId)?.id
...@@ -807,6 +845,33 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -807,6 +845,33 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
const system = await api.system.getSummary(); const system = await api.system.getSummary();
const actionResult = smokeViewMode === "skills" const actionResult = smokeViewMode === "skills"
? await actions.navigateToView("skills") ? await actions.navigateToView("skills")
: smokeViewMode === "settings"
? await (async () => {
await actions.navigateToView("settings");
await waitForSettingsViewReady();
const saved = await actions.saveSettingsConfig({
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"
}
}
});
return {
mode: "settings",
sessionId: "",
skillId: undefined,
settingsSave: saved
};
})()
: await actions.sendConversationPrompt(${JSON.stringify(prompt)}, { : await actions.sendConversationPrompt(${JSON.stringify(prompt)}, {
mode: ${JSON.stringify(smokeViewMode)}, mode: ${JSON.stringify(smokeViewMode)},
projectId: ${JSON.stringify(smokeProjectId)}, projectId: ${JSON.stringify(smokeProjectId)},
...@@ -834,6 +899,7 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -834,6 +899,7 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
skills, skills,
selectedSkillId: actionResult.skillId || selectedSkillId, selectedSkillId: actionResult.skillId || selectedSkillId,
initialSessionId: actionResult.sessionId, initialSessionId: actionResult.sessionId,
settingsSave: actionResult.settingsSave,
system, system,
health: gatewayProbe.health, health: gatewayProbe.health,
status: gatewayProbe.status status: gatewayProbe.status
...@@ -841,6 +907,33 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise< ...@@ -841,6 +907,33 @@ async function runSmokeTest(window: BrowserWindow, outputPath: string): Promise<
})()`); })()`);
await trace("runSmokeTest:send-script-finished"); await trace("runSmokeTest:send-script-finished");
if (smokeViewMode === "settings") {
let finalState: RendererSmokeState | null = null;
const started = Date.now();
while (Date.now() - started < 5000) {
finalState = await waitForRendererSmokeState(window, 1000);
if (finalState?.viewMode === "settings" && matchesExpectedSmokeModelConfig(finalState)) {
break;
}
await delay(100);
}
if (!finalState || finalState.viewMode !== "settings") {
throw new Error("Renderer smoke did not switch to settings view.");
}
if (!matchesExpectedSmokeModelConfig(finalState)) {
throw new Error("Renderer smoke did not publish the saved settings model config in time.");
}
result.sendResult = sendResult;
result.finalState = finalState;
result.ok = true;
await trace("runSmokeTest:settings-view-success");
result.finishedAt = new Date().toISOString();
await trace("runSmokeTest:writing-output");
await writeFile(outputPath, JSON.stringify(result, null, 2), "utf8");
await trace("runSmokeTest:output-written");
app.quit();
return;
}
const streamState = smokeViewMode === "skills" const streamState = smokeViewMode === "skills"
? await waitForRendererSmokeState(window, 5000) ? await waitForRendererSmokeState(window, 5000)
: await waitForRendererStreamSmoke(window, resolveSmokeStreamTimeoutMs()); : await waitForRendererStreamSmoke(window, resolveSmokeStreamTimeoutMs());
......
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"; import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import type { AppConfig, RuntimeModePreference, SaveConfigInput, SetupMode } from "@qjclaw/shared-types"; import type { AppConfig, ExpertModelConfig, RuntimeModePreference, SaveConfigInput, SetupMode } from "@qjclaw/shared-types";
export type RuntimeCloudApiBaseUrlSource = "config" | "env" | "default"; export type RuntimeCloudApiBaseUrlSource = "config" | "env" | "default";
...@@ -46,6 +46,7 @@ interface LegacyConfig { ...@@ -46,6 +46,7 @@ interface LegacyConfig {
cloudApiBaseUrl?: string; cloudApiBaseUrl?: string;
runtimeCloudApiBaseUrl?: string; runtimeCloudApiBaseUrl?: string;
runtimeMode?: RuntimeModePreference; runtimeMode?: RuntimeModePreference;
expertModelConfig?: Partial<Record<keyof ExpertModelConfig, Partial<ExpertModelConfig[keyof ExpertModelConfig]>>>;
} }
function normalizeGatewayUrl(raw: string): string { function normalizeGatewayUrl(raw: string): string {
...@@ -98,6 +99,29 @@ function normalizeRuntimeCloudApiBaseUrl(raw?: string): string { ...@@ -98,6 +99,29 @@ function normalizeRuntimeCloudApiBaseUrl(raw?: string): string {
return normalizeCloudApiBaseUrl(raw ?? ""); return normalizeCloudApiBaseUrl(raw ?? "");
} }
function resolveRuntimeCloudApiTarget(raw?: string): RuntimeCloudApiTarget {
const configValue = migrateDeprecatedRuntimeCloudApiBaseUrl(raw);
if (configValue) {
return {
baseUrl: configValue,
source: "config"
};
}
const envValue = migrateDeprecatedRuntimeCloudApiBaseUrl(process.env.QJCLAW_RUNTIME_CLOUD_API_BASE_URL);
if (envValue) {
return {
baseUrl: envValue,
source: "env"
};
}
return {
baseUrl: DEFAULT_RUNTIME_CLOUD_API_BASE_URL,
source: "default"
};
}
function migrateDeprecatedRuntimeCloudApiBaseUrl(raw?: string): string { function migrateDeprecatedRuntimeCloudApiBaseUrl(raw?: string): string {
const normalized = normalizeRuntimeCloudApiBaseUrl(raw); const normalized = normalizeRuntimeCloudApiBaseUrl(raw);
if (normalized && DEPRECATED_RUNTIME_CLOUD_API_BASE_URLS.has(normalized)) { if (normalized && DEPRECATED_RUNTIME_CLOUD_API_BASE_URLS.has(normalized)) {
...@@ -106,18 +130,41 @@ function migrateDeprecatedRuntimeCloudApiBaseUrl(raw?: string): string { ...@@ -106,18 +130,41 @@ function migrateDeprecatedRuntimeCloudApiBaseUrl(raw?: string): string {
return normalized; return normalized;
} }
function resolveRuntimeCloudApiTarget(raw?: string): RuntimeCloudApiTarget { function createDefaultExpertModelConfig(): ExpertModelConfig {
const normalized = normalizeRuntimeCloudApiBaseUrl(raw); return {
if (normalized) { image: {
return { baseUrl: normalized, source: "config" }; baseUrl: "",
apiKeyConfigured: false
},
video: {
baseUrl: "",
apiKeyConfigured: false
},
copywriting: {
baseUrl: "",
apiKeyConfigured: false
} }
};
}
const envValue = normalizeCloudApiBaseUrl(process.env.QJCLAW_RUNTIME_CLOUD_API_BASE_URL ?? ""); function mergeExpertModelConfig(
if (envValue) { current: ExpertModelConfig,
return { baseUrl: envValue, source: "env" }; input?: SaveConfigInput["expertModelConfig"]
): ExpertModelConfig {
return {
image: {
baseUrl: input?.image?.baseUrl?.trim() ?? current.image.baseUrl,
apiKeyConfigured: typeof input?.image?.apiKey === "string" ? Boolean(input.image.apiKey.trim()) : current.image.apiKeyConfigured
},
video: {
baseUrl: input?.video?.baseUrl?.trim() ?? current.video.baseUrl,
apiKeyConfigured: typeof input?.video?.apiKey === "string" ? Boolean(input.video.apiKey.trim()) : current.video.apiKeyConfigured
},
copywriting: {
baseUrl: input?.copywriting?.baseUrl?.trim() ?? current.copywriting.baseUrl,
apiKeyConfigured: typeof input?.copywriting?.apiKey === "string" ? Boolean(input.copywriting.apiKey.trim()) : current.copywriting.apiKeyConfigured
} }
};
return { baseUrl: DEFAULT_RUNTIME_CLOUD_API_BASE_URL, source: "default" };
} }
export function getRuntimeCloudApiTarget(config: Pick<AppConfig, "runtimeCloudApiBaseUrl">): RuntimeCloudApiTarget { export function getRuntimeCloudApiTarget(config: Pick<AppConfig, "runtimeCloudApiBaseUrl">): RuntimeCloudApiTarget {
...@@ -151,7 +198,8 @@ export class AppConfigService { ...@@ -151,7 +198,8 @@ export class AppConfigService {
gatewayUrl: normalizeGatewayUrl(input.gatewayUrl), gatewayUrl: normalizeGatewayUrl(input.gatewayUrl),
cloudApiBaseUrl: normalizeCloudApiBaseUrl(input.cloudApiBaseUrl), cloudApiBaseUrl: normalizeCloudApiBaseUrl(input.cloudApiBaseUrl),
runtimeCloudApiBaseUrl: migrateDeprecatedRuntimeCloudApiBaseUrl(input.runtimeCloudApiBaseUrl), runtimeCloudApiBaseUrl: migrateDeprecatedRuntimeCloudApiBaseUrl(input.runtimeCloudApiBaseUrl),
runtimeMode: normalizeRuntimeMode(input.runtimeMode) runtimeMode: normalizeRuntimeMode(input.runtimeMode),
expertModelConfig: mergeExpertModelConfig(current.expertModelConfig, input.expertModelConfig)
}; };
await this.writeConfig(config); await this.writeConfig(config);
...@@ -180,11 +228,13 @@ export class AppConfigService { ...@@ -180,11 +228,13 @@ export class AppConfigService {
gatewayUrl: "ws://127.0.0.1:18789", gatewayUrl: "ws://127.0.0.1:18789",
cloudApiBaseUrl: normalizeCloudApiBaseUrl(process.env.QJCLAW_CLOUD_API_BASE_URL ?? ""), cloudApiBaseUrl: normalizeCloudApiBaseUrl(process.env.QJCLAW_CLOUD_API_BASE_URL ?? ""),
runtimeCloudApiBaseUrl: "", runtimeCloudApiBaseUrl: "",
runtimeMode: normalizeRuntimeMode(process.env.QJCLAW_RUNTIME_MODE) runtimeMode: normalizeRuntimeMode(process.env.QJCLAW_RUNTIME_MODE),
expertModelConfig: createDefaultExpertModelConfig()
}; };
} }
private normalizeConfig(config: LegacyConfig): AppConfig { private normalizeConfig(config: LegacyConfig): AppConfig {
const defaultExpertModelConfig = createDefaultExpertModelConfig();
return { return {
setupMode: normalizeSetupMode(config.setupMode), setupMode: normalizeSetupMode(config.setupMode),
provider: config.provider ?? "openai", provider: config.provider ?? "openai",
...@@ -197,7 +247,21 @@ export class AppConfigService { ...@@ -197,7 +247,21 @@ export class AppConfigService {
gatewayUrl: normalizeGatewayUrl(config.gatewayUrl ?? `ws://127.0.0.1:${config.gatewayPort ?? 18789}`), gatewayUrl: normalizeGatewayUrl(config.gatewayUrl ?? `ws://127.0.0.1:${config.gatewayPort ?? 18789}`),
cloudApiBaseUrl: normalizeCloudApiBaseUrl(config.cloudApiBaseUrl ?? process.env.QJCLAW_CLOUD_API_BASE_URL ?? ""), cloudApiBaseUrl: normalizeCloudApiBaseUrl(config.cloudApiBaseUrl ?? process.env.QJCLAW_CLOUD_API_BASE_URL ?? ""),
runtimeCloudApiBaseUrl: migrateDeprecatedRuntimeCloudApiBaseUrl(config.runtimeCloudApiBaseUrl), runtimeCloudApiBaseUrl: migrateDeprecatedRuntimeCloudApiBaseUrl(config.runtimeCloudApiBaseUrl),
runtimeMode: normalizeRuntimeMode(config.runtimeMode ?? process.env.QJCLAW_RUNTIME_MODE) runtimeMode: normalizeRuntimeMode(config.runtimeMode ?? process.env.QJCLAW_RUNTIME_MODE),
expertModelConfig: {
image: {
baseUrl: config.expertModelConfig?.image?.baseUrl?.trim() ?? defaultExpertModelConfig.image.baseUrl,
apiKeyConfigured: Boolean(config.expertModelConfig?.image?.apiKeyConfigured)
},
video: {
baseUrl: config.expertModelConfig?.video?.baseUrl?.trim() ?? defaultExpertModelConfig.video.baseUrl,
apiKeyConfigured: Boolean(config.expertModelConfig?.video?.apiKeyConfigured)
},
copywriting: {
baseUrl: config.expertModelConfig?.copywriting?.baseUrl?.trim() ?? defaultExpertModelConfig.copywriting.baseUrl,
apiKeyConfigured: Boolean(config.expertModelConfig?.copywriting?.apiKeyConfigured)
}
}
}; };
} }
......
...@@ -7,6 +7,9 @@ interface SecretRecord { ...@@ -7,6 +7,9 @@ interface SecretRecord {
gatewayToken?: string; gatewayToken?: string;
deviceToken?: string; deviceToken?: string;
authToken?: string; authToken?: string;
imageModelApiKey?: string;
videoModelApiKey?: string;
copywritingModelApiKey?: string;
} }
interface SecretAccessor { interface SecretAccessor {
...@@ -14,7 +17,7 @@ interface SecretAccessor { ...@@ -14,7 +17,7 @@ interface SecretAccessor {
set(secretName: SecretName, value?: string): Promise<void>; set(secretName: SecretName, value?: string): Promise<void>;
} }
type SecretName = "apiKey" | "gatewayToken" | "deviceToken" | "authToken"; type SecretName = "apiKey" | "gatewayToken" | "deviceToken" | "authToken" | "imageModelApiKey" | "videoModelApiKey" | "copywritingModelApiKey";
type KeytarModule = typeof import("keytar"); type KeytarModule = typeof import("keytar");
const KEYTAR_SERVICE = "QianjiangClaw"; const KEYTAR_SERVICE = "QianjiangClaw";
...@@ -23,7 +26,10 @@ const KEYTAR_ACCOUNT_MAP: Record<SecretName, string> = { ...@@ -23,7 +26,10 @@ const KEYTAR_ACCOUNT_MAP: Record<SecretName, string> = {
apiKey: "provider-api-key", apiKey: "provider-api-key",
gatewayToken: "gateway-token", gatewayToken: "gateway-token",
deviceToken: "gateway-device-token", deviceToken: "gateway-device-token",
authToken: "cloud-auth-token" authToken: "cloud-auth-token",
imageModelApiKey: "image-model-api-key",
videoModelApiKey: "video-model-api-key",
copywritingModelApiKey: "copywriting-model-api-key"
}; };
class FileSecretStore implements SecretAccessor { class FileSecretStore implements SecretAccessor {
...@@ -163,6 +169,30 @@ export class SecretManager { ...@@ -163,6 +169,30 @@ export class SecretManager {
return this.store.get("authToken"); return this.store.get("authToken");
} }
async setImageModelApiKey(apiKey?: string): Promise<void> {
await this.store.set("imageModelApiKey", apiKey);
}
async getImageModelApiKey(): Promise<string | undefined> {
return this.store.get("imageModelApiKey");
}
async setVideoModelApiKey(apiKey?: string): Promise<void> {
await this.store.set("videoModelApiKey", apiKey);
}
async getVideoModelApiKey(): Promise<string | undefined> {
return this.store.get("videoModelApiKey");
}
async setCopywritingModelApiKey(apiKey?: string): Promise<void> {
await this.store.set("copywritingModelApiKey", apiKey);
}
async getCopywritingModelApiKey(): Promise<string | undefined> {
return this.store.get("copywritingModelApiKey");
}
private async tryLoadKeytar(): Promise<KeytarModule | null> { private async tryLoadKeytar(): Promise<KeytarModule | null> {
try { try {
const imported = await import("keytar"); const imported = await import("keytar");
...@@ -173,7 +203,7 @@ export class SecretManager { ...@@ -173,7 +203,7 @@ export class SecretManager {
} }
private async migrateFallbackSecrets(): Promise<void> { private async migrateFallbackSecrets(): Promise<void> {
for (const secretName of ["apiKey", "gatewayToken", "deviceToken", "authToken"] as const) { for (const secretName of ["apiKey", "gatewayToken", "deviceToken", "authToken", "imageModelApiKey", "videoModelApiKey", "copywritingModelApiKey"] as const) {
const existing = await this.store.get(secretName); const existing = await this.store.get(secretName);
if (existing) { if (existing) {
continue; continue;
......
...@@ -344,6 +344,37 @@ if (smokeViewMode === 'skills') { ...@@ -344,6 +344,37 @@ if (smokeViewMode === 'skills') {
if (typeof sendResult.workspaceSkillCount !== 'number') { if (typeof sendResult.workspaceSkillCount !== 'number') {
throw new Error('Skills smoke did not report workspaceSkillCount.'); throw new Error('Skills smoke did not report workspaceSkillCount.');
} }
} else if (smokeViewMode === 'settings') {
if (String(finalState.viewMode || '') !== 'settings') {
throw new Error('Settings smoke did not end on settings view: ' + String(finalState.viewMode || ''));
}
const settingsSave = sendResult.settingsSave || {};
const modelConfig = finalState.modelConfig || {};
const config = finalState.config || {};
if (!settingsSave.expertModelConfig) {
throw new Error('Settings smoke did not report saved expertModelConfig.');
}
if (String(modelConfig.image && modelConfig.image.baseUrl || '') !== 'https://image-smoke.example.com/v1') {
throw new Error('Settings smoke did not persist image model baseUrl.');
}
if (String(modelConfig.video && modelConfig.video.baseUrl || '') !== 'https://video-smoke.example.com/v1') {
throw new Error('Settings smoke did not persist video model baseUrl.');
}
if (String(modelConfig.copywriting && modelConfig.copywriting.baseUrl || '') !== 'https://copy-smoke.example.com/v1') {
throw new Error('Settings smoke did not persist copywriting model baseUrl.');
}
if (!Boolean(modelConfig.image && modelConfig.image.apiKeyConfigured)) {
throw new Error('Settings smoke did not mark image model api key as configured.');
}
if (!Boolean(modelConfig.video && modelConfig.video.apiKeyConfigured)) {
throw new Error('Settings smoke did not mark video model api key as configured.');
}
if (!Boolean(modelConfig.copywriting && modelConfig.copywriting.apiKeyConfigured)) {
throw new Error('Settings smoke did not mark copywriting model api key as configured.');
}
if (String(config.runtimeMode || '') !== 'bundled-runtime') {
throw new Error('Settings smoke unexpectedly changed runtimeMode: ' + String(config.runtimeMode || ''));
}
} else { } else {
const executionPolicySource = String(streamSmoke.executionPolicySource || ''); const executionPolicySource = String(streamSmoke.executionPolicySource || '');
const statusLabels = Array.isArray(streamSmoke.statusLabels) const statusLabels = Array.isArray(streamSmoke.statusLabels)
...@@ -394,7 +425,7 @@ const acceptWorkspaceLaunch = process.env.QJCLAW_SMOKE_ACCEPT_WORKSPACE_LAUNCH = ...@@ -394,7 +425,7 @@ const acceptWorkspaceLaunch = process.env.QJCLAW_SMOKE_ACCEPT_WORKSPACE_LAUNCH =
if (!sendResult.runtimeCloudFetch || sendResult.runtimeCloudFetch.state !== 'ready') { if (!sendResult.runtimeCloudFetch || sendResult.runtimeCloudFetch.state !== 'ready') {
throw new Error('Runtime cloud config fetch did not succeed.'); throw new Error('Runtime cloud config fetch did not succeed.');
} }
if (smokeViewMode !== 'skills') { if (!['skills', 'settings'].includes(smokeViewMode)) {
if (!diagnosticsPath || !fs.existsSync(diagnosticsPath)) { if (!diagnosticsPath || !fs.existsSync(diagnosticsPath)) {
throw new Error('Diagnostics snapshot was not produced by smoke.'); throw new Error('Diagnostics snapshot was not produced by smoke.');
} }
......
param(
[int]$SmokePort = 4318,
[string]$SmokeToken = 'smoke-token',
[string]$BaseOutputDir,
[int]$TimeoutSeconds = 180
)
$ErrorActionPreference = 'Stop'
function Invoke-ElectronSmokeWithRetry {
param(
[string]$ScriptPath,
[string]$Label,
[string[]]$ArgumentList,
[int]$MaxAttempts = 2
)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt += 1) {
powershell -ExecutionPolicy Bypass -File $ScriptPath @ArgumentList
if ($LASTEXITCODE -eq 0) {
return
}
if ($attempt -ge $MaxAttempts) {
exit $LASTEXITCODE
}
Write-Warning "$Label failed on attempt $attempt. Retrying..."
Start-Sleep -Seconds 2
}
}
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
if (-not $BaseOutputDir) {
$BaseOutputDir = Join-Path $repoRoot '.tmp\settings-smoke'
}
$BaseOutputDir = [System.IO.Path]::GetFullPath($BaseOutputDir)
$userDataPath = Join-Path $BaseOutputDir 'user-data'
$logsPath = Join-Path $BaseOutputDir 'logs'
$smokeOutput = Join-Path $BaseOutputDir 'result.json'
$electronSmokeScript = Join-Path $repoRoot 'build\scripts\electron-smoke.ps1'
if (Test-Path $BaseOutputDir) {
Remove-Item $BaseOutputDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $BaseOutputDir, $userDataPath, $logsPath | Out-Null
Invoke-ElectronSmokeWithRetry -ScriptPath $electronSmokeScript -Label 'settings smoke' -ArgumentList @(
'-SmokeOutput', $smokeOutput,
'-SmokePort', $SmokePort,
'-SmokeToken', $SmokeToken,
'-UserDataPath', $userDataPath,
'-LogsPath', $logsPath,
'-RuntimeMode', 'bundled-runtime',
'-SmokeViewMode', 'settings',
'-TimeoutSeconds', $TimeoutSeconds
)
...@@ -39,6 +39,20 @@ function createConfig(overrides: Partial<AppConfig> = {}): AppConfig { ...@@ -39,6 +39,20 @@ function createConfig(overrides: Partial<AppConfig> = {}): AppConfig {
cloudApiBaseUrl: "https://cloud.example.com", cloudApiBaseUrl: "https://cloud.example.com",
runtimeCloudApiBaseUrl: "https://cloud.example.com", runtimeCloudApiBaseUrl: "https://cloud.example.com",
runtimeMode: "bundled-runtime", runtimeMode: "bundled-runtime",
expertModelConfig: {
image: {
baseUrl: "",
apiKeyConfigured: false
},
video: {
baseUrl: "",
apiKeyConfigured: false
},
copywriting: {
baseUrl: "",
apiKeyConfigured: false
}
},
...overrides ...overrides
}; };
} }
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
"launch:xhs-local-manual": "powershell -ExecutionPolicy Bypass -File build/scripts/xhs-expert-manual-launch.ps1", "launch:xhs-local-manual": "powershell -ExecutionPolicy Bypass -File build/scripts/xhs-expert-manual-launch.ps1",
"launch:douyin-local-manual": "powershell -ExecutionPolicy Bypass -File build/scripts/douyin-expert-manual-launch.ps1", "launch:douyin-local-manual": "powershell -ExecutionPolicy Bypass -File build/scripts/douyin-expert-manual-launch.ps1",
"smoke:default-chat": "powershell -ExecutionPolicy Bypass -File build/scripts/default-chat-smoke.ps1", "smoke:default-chat": "powershell -ExecutionPolicy Bypass -File build/scripts/default-chat-smoke.ps1",
"smoke:settings": "powershell -ExecutionPolicy Bypass -File build/scripts/settings-smoke.ps1",
"smoke:project-routing": "powershell -ExecutionPolicy Bypass -File build/scripts/project-routing-smoke.ps1", "smoke:project-routing": "powershell -ExecutionPolicy Bypass -File build/scripts/project-routing-smoke.ps1",
"smoke:project-package-orchestrator": "powershell -ExecutionPolicy Bypass -File build/scripts/project-package-orchestrator-smoke.ps1", "smoke:project-package-orchestrator": "powershell -ExecutionPolicy Bypass -File build/scripts/project-package-orchestrator-smoke.ps1",
"smoke:project-isolation": "powershell -ExecutionPolicy Bypass -File build/scripts/project-isolation-smoke.ps1", "smoke:project-isolation": "powershell -ExecutionPolicy Bypass -File build/scripts/project-isolation-smoke.ps1",
......
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