fix: harden model UI and desktop packaging

This commit is contained in:
linshen committed 2026-09-10 21:47:36 +08:00
1 parent 5b995b94ea
commit cc223808f5
15 files changed
+149 -39

No files matched your search

+1 -3
View File
@@ -46,7 +46,5 @@ jobs:
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
- name: Run gate E2E checks (VCR replay)
- name: Run real UI smoke and gate E2E checks
run: pnpm test:gate:e2e
env:
E2E_VCR_MODE: replay
+3 -1
View File
@@ -34,14 +34,16 @@
"check:extension-release-secrets": "node scripts/check-extension-release-secrets.mjs packages/extension/dist",
"test:e2e": "playwright test",
"test:e2e:smart": "node scripts/smart-e2e.js",
"test:e2e:ui-smoke": "cross-env E2E_VCR_MODE=live E2E_EMPTY_MODEL_CONFIG=1 node scripts/run-e2e-group.js uiSmoke",
"test:e2e:gate": "node scripts/run-e2e-group.js gate",
"test:e2e:gate:replay": "cross-env E2E_VCR_MODE=replay node scripts/run-e2e-group.js gate",
"test:e2e:extended": "node scripts/run-e2e-group.js extended",
"test:e2e:record": "node scripts/run-e2e-record.js",
"test:e2e:replay": "cross-env E2E_VCR_MODE=replay node scripts/run-e2e-group.js extended",
"compare:calibrate": "pnpm -F @prompt-optimizer/core build && node scripts/run-structured-compare-calibration.mjs",
"test:gate:core": "pnpm -F @prompt-optimizer/core test:gate",
"test:gate:ui": "pnpm -F @prompt-optimizer/core build && pnpm -F @prompt-optimizer/ui test",
"test:gate:e2e": "pnpm test:e2e:gate",
"test:gate:e2e": "node scripts/run-many.js -s test:e2e:ui-smoke test:e2e:gate:replay",
"test:gate": "node scripts/run-many.js -s test:repo test:gate:core test:gate:ui",
"test:gate:full": "node scripts/run-many.js -s test:gate test:gate:e2e",
"test:fast": "pnpm -r test --run --passWithNoTests",
+1
View File
@@ -43,6 +43,7 @@
"preload.js",
"remote-storage.js",
"config/**/*",
"icons/**/*",
"web-dist/**/*",
"node_modules/**/*"
],
+9 -2
View File
@@ -64,8 +64,15 @@
</NButton>
</template>
<div class="model-manager-content">
<NTabs v-model:value="activeTab" type="segment" size="small" animated class="model-manager-tabs">
<div class="model-manager-content" data-testid="model-manager-dialog">
<NTabs
v-model:value="activeTab"
data-testid="model-manager-tabs"
type="segment"
size="small"
animated
class="model-manager-tabs"
>
<NTabPane name="text" :tab="t('modelManager.textModels')" />
<NTabPane name="image" :tab="t('modelManager.imageModels')" />
<NTabPane name="function" :tab="t('modelManager.functionModels')" />
@@ -13,6 +13,7 @@
<NText depth="3">{{ emptyText || t('model.select.noAvailableModels') }}</NText>
<NButton
v-if="shouldShowEmptyConfigCTA"
data-testid="select-empty-config-action"
type="tertiary"
size="small"
ghost
@@ -101,6 +101,7 @@
<template #model-select>
<SelectWithConfig
v-model="selectedOptimizeModelKeyModel"
data-testid="basic-system-model-select"
:options="modelSelection.textModelOptions"
:getPrimary="OptionAccessors.getPrimary"
:getSecondary="OptionAccessors.getSecondary"
+27
View File
@@ -11,6 +11,30 @@ const E2E_PORT = process.env.E2E_PORT || 15555;
const BASE_URL = `http://localhost:${E2E_PORT}`;
const E2E_VCR_MODE = process.env.E2E_VCR_MODE || 'auto';
const USE_VCR_PLACEHOLDER_KEYS = E2E_VCR_MODE === 'replay';
const USE_EMPTY_MODEL_CONFIG = process.env.E2E_EMPTY_MODEL_CONFIG === '1';
const MODEL_CREDENTIAL_ENV_KEYS = [
'VITE_ANTHROPIC_API_KEY',
'VITE_ARK_API_KEY',
'VITE_CF_ACCOUNT_ID',
'VITE_CF_API_TOKEN',
'VITE_CUSTOM_API_BASE_URL',
'VITE_CUSTOM_API_HEADERS',
'VITE_CUSTOM_API_KEY',
'VITE_CUSTOM_API_MODEL',
'VITE_DASHSCOPE_API_KEY',
'VITE_DEEPSEEK_API_KEY',
'VITE_GEMINI_API_KEY',
'VITE_GROK_API_KEY',
'VITE_MIMO_TOKEN_PLAN_API_KEY',
'VITE_MINIMAX_API_KEY',
'VITE_MODELSCOPE_API_KEY',
'VITE_OPENAI_API_KEY',
'VITE_OPENROUTER_API_KEY',
'VITE_SEEDREAM_API_KEY',
'VITE_SILICONFLOW_API_KEY',
'VITE_XAI_API_KEY',
'VITE_ZHIPU_API_KEY',
] as const;
export default defineConfig({
// 测试目录
@@ -90,6 +114,9 @@ export default defineConfig({
// 避免因本机缺少真实 key 而导致 UI 不渲染对应选项,从而无法命中既有 VCR fixtures。
env: {
...process.env,
...(USE_EMPTY_MODEL_CONFIG
? Object.fromEntries(MODEL_CREDENTIAL_ENV_KEYS.map((key) => [key, '']))
: {}),
...(USE_VCR_PLACEHOLDER_KEYS
? {
VITE_SILICONFLOW_API_KEY: process.env.VITE_SILICONFLOW_API_KEY || 'vcr',
+3
View File
@@ -1,4 +1,7 @@
const groups = {
uiSmoke: [
'tests/e2e/regression/model-configuration-smoke.spec.ts',
],
gate: [
'tests/e2e/workflows/p0-route-smoke.spec.ts',
'tests/e2e/regression/root-route-bootstrap.spec.ts',
+15
View File
@@ -90,3 +90,18 @@ test('mcp-server bin points to a file that exists before build output is generat
`Expected ${binTargetPath} to exist so pnpm can create the workspace bin shim during install`,
)
})
test('desktop package includes every runtime window icon', () => {
const desktopPackage = readJson(path.join('packages', 'desktop', 'package.json'))
const packagedFiles = desktopPackage.build?.files ?? []
assert.ok(
packagedFiles.includes('icons/**/*'),
'Desktop build.files must include the icons directory used by main.js at runtime',
)
for (const iconName of ['app-icon.ico', 'app-icon.icns', 'app-icon.png']) {
const iconPath = path.join(process.cwd(), 'packages', 'desktop', 'icons', iconName)
assert.equal(fs.existsSync(iconPath), true, `Expected runtime icon ${iconPath} to exist`)
}
})
+39
View File
@@ -0,0 +1,39 @@
const { spawn, spawnSync } = require('node:child_process')
function getPnpmRunnerSpec(platform = process.platform, env = process.env) {
if (platform === 'win32') {
return {
command: env.ComSpec || env.COMSPEC || 'cmd.exe',
argsPrefix: ['/d', '/s', '/c', 'pnpm'],
shell: false,
}
}
return {
command: 'pnpm',
argsPrefix: [],
shell: false,
}
}
function spawnPnpm(args, options = {}) {
const { command, argsPrefix, shell } = getPnpmRunnerSpec()
return spawn(command, [...argsPrefix, ...args], {
shell,
...options,
})
}
function spawnPnpmSync(args, options = {}) {
const { command, argsPrefix, shell } = getPnpmRunnerSpec()
return spawnSync(command, [...argsPrefix, ...args], {
shell,
...options,
})
}
module.exports = {
getPnpmRunnerSpec,
spawnPnpm,
spawnPnpmSync,
}
+3 -5
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
const { spawnSync } = require('child_process')
const { groups } = require('./e2e-groups')
const { spawnPnpmSync } = require('./pnpm-runner')
function runGroup(groupName, extraArgs = []) {
const specs = groups[groupName]
@@ -17,11 +17,9 @@ function runGroup(groupName, extraArgs = []) {
}
console.log('')
const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const result = spawnSync(pnpmCommand, ['exec', 'playwright', 'test', ...specs, ...extraArgs], {
const result = spawnPnpmSync(['exec', 'playwright', 'test', ...specs, ...extraArgs], {
stdio: 'inherit',
env: process.env,
shell: process.platform === 'win32'
env: process.env
})
if (result.error) {
+3 -5
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
const { spawnSync } = require('child_process')
const { spawnPnpmSync } = require('./pnpm-runner')
const extraArgs = process.argv.slice(2)
@@ -10,14 +10,12 @@ if (extraArgs.length === 0) {
process.exit(1)
}
const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
const result = spawnSync(pnpmCommand, ['exec', 'playwright', 'test', ...extraArgs], {
const result = spawnPnpmSync(['exec', 'playwright', 'test', ...extraArgs], {
stdio: 'inherit',
env: {
...process.env,
E2E_VCR_MODE: 'record'
},
shell: process.platform === 'win32'
}
})
if (result.error) {
+3 -19
View File
@@ -1,20 +1,6 @@
#!/usr/bin/env node
const { spawn } = require('node:child_process');
function getRunnerSpec(platform = process.platform) {
if (platform === 'win32') {
return {
command: 'pnpm',
shell: true,
};
}
return {
command: 'pnpm',
shell: false,
};
}
const { getPnpmRunnerSpec, spawnPnpm } = require('./pnpm-runner');
function parseArgs(argv) {
let parallel = false;
@@ -40,10 +26,8 @@ function parseArgs(argv) {
function runPackageScript(script, options = {}) {
return new Promise((resolve, reject) => {
const { command, shell } = getRunnerSpec();
const child = spawn(command, ['run', script], {
const child = spawnPnpm(['run', script], {
stdio: 'inherit',
shell,
env: process.env,
...options,
});
@@ -90,7 +74,7 @@ if (require.main === module) {
}
module.exports = {
getRunnerSpec,
getRunnerSpec: getPnpmRunnerSpec,
main,
parseArgs,
runPackageScript,
+6 -4
View File
@@ -21,13 +21,15 @@ test('parseArgs supports parallel aliases', () => {
});
});
test('getRunnerSpec uses shell mode for Windows pnpm invocation', () => {
assert.deepEqual(getRunnerSpec('win32'), {
command: 'pnpm',
shell: true,
test('getRunnerSpec avoids shell mode while preserving Windows pnpm compatibility', () => {
assert.deepEqual(getRunnerSpec('win32', { ComSpec: 'C:\\Windows\\System32\\cmd.exe' }), {
command: 'C:\\Windows\\System32\\cmd.exe',
argsPrefix: ['/d', '/s', '/c', 'pnpm'],
shell: false,
});
assert.deepEqual(getRunnerSpec('linux'), {
command: 'pnpm',
argsPrefix: [],
shell: false,
});
});
@@ -0,0 +1,34 @@
import { test, expect } from '../fixtures'
test.describe('Model configuration smoke', () => {
test('first-use model selector opens a manager with switchable text, image, and function tabs', async ({ page }) => {
await page.goto('/')
await expect(page.locator('[data-testid="workspace"][data-mode="basic-system"]')).toBeVisible({
timeout: 45000,
})
const modelSelect = page.getByTestId('basic-system-model-select')
await expect(modelSelect).toBeVisible()
await modelSelect.click()
const emptyConfigAction = page.getByTestId('select-empty-config-action')
await expect(emptyConfigAction).toBeVisible()
await expect(page.getByText(/暂无可用模型|No available models/)).toBeVisible()
await emptyConfigAction.click()
const dialog = page.getByTestId('model-manager-dialog')
await expect(dialog).toBeVisible()
const tabs = page.getByTestId('model-manager-tabs')
const tabItems = tabs.locator('.n-tabs-tab')
await expect(tabs).toContainText(/文本模型|Text Models/)
await expect(tabs).toContainText(/图像模型|Image Models/)
await expect(tabs).toContainText(/功能模型|Function Models/)
for (const tabLabel of [/图像模型|Image Models/, /功能模型|Function Models/, /文本模型|Text Models/]) {
const tab = tabItems.filter({ hasText: tabLabel }).first()
await tab.click()
await expect(tab).toHaveClass(/n-tabs-tab--active/)
}
})
})