From 7ebe9092ba29a6674d6f5c4153f31214dd6f2319 Mon Sep 17 00:00:00 2001 From: cen617-code <1057290604@qq.com> Date: Mon, 28 Sep 2026 11:56:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(website):=20=E7=A7=BB=E9=99=A4=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=AF=BC=E5=85=A5=E4=B8=AD=E7=9A=84=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E8=AE=AD=E7=BB=83=E5=9C=BA=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++ docs/website-deployment.md | 4 +- web_platform/e2e/website.import.spec.ts | 52 ++++++++++++++++ web_platform/playwright.production.config.ts | 6 +- web_platform/playwright.website.config.ts | 2 +- .../UrdfImportOptionsDialog.test.tsx | 62 +++++++++++++++++++ .../components/UrdfImportOptionsDialog.tsx | 21 +++++-- 7 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 web_platform/e2e/website.import.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 36fcc017..c8d84614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## [未发布] +### 网站导入选项清理(2026-09-28) + +- 网站版 URDF/ZIP 导入弹窗移除 LeKiwi v1、LeKiwi Bundle 的“移动操作训练场景”选项及训练提示;不沿用已移除的初始训练选择。保留通用转换、普通 LeKiwi profile、语言控制与本地版训练功能。 +- 新增网站/本地边界单测及 URDF/ZIP 真实 WASM 导入回归。TypeScript、定向 ESLint/格式检查、137 文件/680 项前端单测、48 项后端测试(另有 3 项既有可选门禁跳过)、网站/本地双构建和网站假上游回归 5/5 通过。 +- 已发布网站 `20260928T035026Z-import-options`,回滚版本为 `20260924T081555Z-language-render-fix`;公网非付费检查 8/8 通过,HTML/主 JS 与本次构建逐字节一致。沿用既有后端与代理配置,密钥文件、持久预算目录及 iframe 白名单不变;本次无付费模型调用。发布证据位于 `build/website-deployment/import-options-20260928/`。 + ## [1.0.3] - 2026-09-24 ### 预设场景清晰度与语言搬运监督修复 diff --git a/docs/website-deployment.md b/docs/website-deployment.md index bb9a8d3e..ae2cbe9c 100644 --- a/docs/website-deployment.md +++ b/docs/website-deployment.md @@ -2,7 +2,7 @@ 目标:`https://cadworld-sim.robotquan.com`,主机 `root@47.93.31.109`。复用 1Panel 的 OpenResty,独立容器仅发布 `127.0.0.1:8768`。不安装训练/GPU 服务,不改系统 Python,不需要访问者开启终端。 -当前在线版本:`20260924T081555Z-language-render-fix`,上一版本 `20260924T065821Z-language-v2`。前后端共同发布,修复预设阴影/高 DPI 渲染与 Jev 技能上下文;公网非付费检查 6/6 及硬件双相机验证通过。详见 [修复与发布记录](website-language-render-fix.md),其中包含保留 iframe 热更新的回滚步骤。 +当前在线版本:`20260928T035026Z-import-options`,上一版本 `20260924T081555Z-language-render-fix`。网站 URDF/ZIP 导入弹窗已移除两项“移动操作训练场景”及训练提示,普通导入、语言控制和本地训练保留。沿用原后端/代理配置,通过现有成对发布流程上线;后端容器已重启,旧访客会话失效,密钥文件与持久预算目录保持不变。公网非付费检查 8/8 通过(含两种文件导入、预设/双相机、Python、会话安全和 iframe),HTML/主 JS 与构建产物一致;未调用付费模型。证据:`build/website-deployment/import-options-20260928/`。此前渲染与语言监督修复详见 [修复与发布记录](website-language-render-fix.md)。 iframe 嵌入已通过响应头热更新放行 `https://cadworld.robotquan.com`,未重启应用;接入示例、验证范围与配置回滚见 [嵌入说明](website-embedding.md)。重新发布旧包会覆盖该热更新,应从当前源码重新构建。 @@ -11,7 +11,7 @@ iframe 嵌入已通过响应头热更新放行 `https://cadworld.robotquan.com` - 网站公开、无需注册或前端输入密钥。按用户新要求固定服务器 DeepSeek `deepseek-flash` 和 OpenRouter Jev `typesafe/jev-1.13`,费用由站长承担;前端移除模型/密钥/订阅配置。 - 官方订阅仍在旧本地/非共享服务实现中保留,但当前共享密钥网站关闭其接口(403),没有绕过官方网络限制或登录其他账号。 - 仅两项授权密钥存于 `/opt/cadworld-sim/secrets/model-keys.env`(0400、UID 10001,父目录 root/0700),只读挂载;不进入镜像、静态目录、发布包、浏览器或日志。每访客任务状态独立,空闲 30 分钟/最长 8 小时销毁;删除会话不会删除站长的服务器配置。 -- ZIP/URDF/地图/物理仿真仍在浏览器。网站隐藏训练、调参和外控,浏览器 Python/ONNX 保留;本地训练/外控功能保留,语言面板与网站统一精简并使用同源模型后端。 +- ZIP/URDF/地图/物理仿真仍在浏览器。网站隐藏训练、调参和外控,导入弹窗不提供移动操作训练场景;通用转换、普通 LeKiwi profile、浏览器 Python/ONNX 保留。本地训练/外控功能保留,语言面板与网站统一精简并使用同源模型后端。 - 容量:128 会话、每会话 1 推理/120 次每小时,生产全站并行上限 2。共享 SQLite 限额为每小时 60、滚动 24h 600 次上游调用;`/command` 保守预留 2 次,失败不退款,不自动重试。预算在 `/opt/cadworld-sim/budget`,不得在重启/发布时删除。匿名访问仍可能耗尽公共额度;建议供应商侧另设金额上限,当前不是货币金额硬预算。 - 备案已由站长确认;正式备案/公安备案展示号码由站长提供,不能编造。TLS 证书首次无邮箱注册,自动续期由 systemd timer 负责,建议管理员另行设置证书到期监控。 diff --git a/web_platform/e2e/website.import.spec.ts b/web_platform/e2e/website.import.spec.ts new file mode 100644 index 00000000..de013f02 --- /dev/null +++ b/web_platform/e2e/website.import.spec.ts @@ -0,0 +1,52 @@ +import { expect, test } from '@playwright/test'; +import { resolve } from 'node:path'; + +for (const kind of ['URDF', 'ZIP'] as const) { + test(`网站 ${kind} 导入不显示移动操作训练场景,普通导入仍可用`, async ({ page }, info) => { + let inference = 0; + const errors: string[] = []; + page.on('pageerror', (error) => errors.push(error.message)); + await page.route('**/api/decision/v1/**', async (route) => { + if (/\/(command|plan|decide|test)$/.test(new URL(route.request().url()).pathname)) { + inference++; + await route.abort(); + } else await route.continue(); + }); + expect((await page.goto('/'))?.status()).toBe(200); + await page + .locator('input[type=file]') + .first() + .setInputFiles( + kind === 'ZIP' + ? resolve('web_platform/assets/robots/lekiwi-v1.zip') + : { + name: 'robot.urdf', + mimeType: 'application/xml', + buffer: Buffer.from(` + + + `), + }, + ); + const dialog = page.getByRole('dialog', { name: '配置 URDF 仿真组件' }); + await expect(dialog).toBeVisible({ timeout: 30000 }); + const profile = dialog.getByLabel('URDF 机器人 profile'); + await expect(profile).toHaveValue(''); + await expect(profile.locator('option')).toHaveText([ + '不启用(通用转换)', + 'LeKiwi v1(仿真专用)', + ]); + await expect(dialog.getByText(/移动操作训练场景|强化学习任务即可一键训练/)).toHaveCount(0); + await expect(dialog.getByRole('checkbox', { name: /为关节添加驱动器/ })).toBeEnabled(); + await page.screenshot({ path: info.outputPath('import-options.png') }); + if (kind === 'ZIP') await profile.selectOption('lekiwi-v1'); + await dialog.getByRole('button', { name: '转换并加载' }).click(); + await expect(dialog).not.toBeVisible(); + await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 90000 }); + await page.getByRole('tab', { name: '控制台', exact: true }).click(); + await expect(page.getByRole('button', { name: '强化学习任务', exact: true })).toHaveCount(0); + await expect(page.getByRole('button', { name: '机器人语言控制', exact: true })).toBeVisible(); + expect(inference).toBe(0); + expect(errors).toEqual([]); + }); +} diff --git a/web_platform/playwright.production.config.ts b/web_platform/playwright.production.config.ts index 02b33551..9c484f07 100644 --- a/web_platform/playwright.production.config.ts +++ b/web_platform/playwright.production.config.ts @@ -1,7 +1,11 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './e2e', - testMatch: ['website.production.spec.ts', 'website.embed.production.spec.ts'], + testMatch: [ + 'website.production.spec.ts', + 'website.embed.production.spec.ts', + ...(process.env.CADWORLD_PRODUCTION_TEST === '1' ? ['website.import.spec.ts'] : []), + ], workers: 1, timeout: 240_000, use: { diff --git a/web_platform/playwright.website.config.ts b/web_platform/playwright.website.config.ts index 5c12d640..8c73aee8 100644 --- a/web_platform/playwright.website.config.ts +++ b/web_platform/playwright.website.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ testDir: './e2e', - testMatch: 'website.spec.ts', + testMatch: ['website.spec.ts', 'website.import.spec.ts'], workers: 1, timeout: 240_000, use: { diff --git a/web_platform/src/app/components/UrdfImportOptionsDialog.test.tsx b/web_platform/src/app/components/UrdfImportOptionsDialog.test.tsx index 542b881e..6da9fc18 100644 --- a/web_platform/src/app/components/UrdfImportOptionsDialog.test.tsx +++ b/web_platform/src/app/components/UrdfImportOptionsDialog.test.tsx @@ -1,7 +1,69 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { UrdfImportOptionsDialog } from './UrdfImportOptionsDialog'; +const capabilities = vi.hoisted(() => ({ training: true })); +vi.mock('../websiteConfig', () => ({ websiteCapabilities: capabilities })); + +const mobileProfiles = ['MobileManipulator-LeKiwi-v1', 'MobileManipulator-LeKiwi-Bundle']; + describe('UrdfImportOptionsDialog', () => { + beforeEach(() => { + capabilities.training = true; + }); + + it.each(mobileProfiles)('本地版保留训练场景 %s', (profileId) => { + const onConfirm = vi.fn(); + render( {}} />); + expect(screen.getAllByRole('option', { name: /移动操作训练场景/ })).toHaveLength(2); + fireEvent.change(screen.getByLabelText('URDF 机器人 profile'), { + target: { value: profileId }, + }); + expect(screen.getByText(/强化学习任务即可一键训练/)).toBeVisible(); + fireEvent.click(screen.getByRole('button', { name: '转换并加载' })); + expect(onConfirm).toHaveBeenCalledWith( + { addActuators: false, addSensors: false, sensorType: 'camera' }, + profileId, + ); + }); + + it('网站版移除训练选项,仍可选择普通 LeKiwi profile', () => { + capabilities.training = false; + const onConfirm = vi.fn(); + render( {}} />); + expect(screen.queryAllByRole('option', { name: /移动操作训练场景/ })).toHaveLength(0); + expect(screen.queryByText(/强化学习任务即可一键训练/)).not.toBeInTheDocument(); + expect(screen.getByRole('option', { name: '不启用(通用转换)' })).toBeInTheDocument(); + fireEvent.change(screen.getByLabelText('URDF 机器人 profile'), { + target: { value: 'lekiwi-v1' }, + }); + fireEvent.click(screen.getByRole('button', { name: '转换并加载' })); + expect(onConfirm).toHaveBeenCalledWith( + { addActuators: false, addSensors: false, sensorType: 'camera' }, + 'lekiwi-v1', + ); + }); + + it.each(mobileProfiles)('网站版不沿用已移除的初始训练场景 %s', (profileId) => { + capabilities.training = false; + const onConfirm = vi.fn(); + render( + {}} + />, + ); + expect(screen.getByLabelText('URDF 机器人 profile')).toHaveValue(''); + expect(screen.queryByText(/强化学习任务即可一键训练/)).not.toBeInTheDocument(); + expect(screen.getByRole('checkbox', { name: /为关节添加驱动器/ })).toBeEnabled(); + expect(screen.getByRole('button', { name: '不添加,直接加载' })).toBeEnabled(); + fireEvent.click(screen.getByRole('button', { name: '转换并加载' })); + expect(onConfirm).toHaveBeenCalledWith( + expect.objectContaining({ addActuators: true, addSensors: true }), + ); + }); + it('默认选择关节驱动器和摄像头传感器', () => { const onConfirm = vi.fn(); render( diff --git a/web_platform/src/app/components/UrdfImportOptionsDialog.tsx b/web_platform/src/app/components/UrdfImportOptionsDialog.tsx index 5a53046b..8e7a0ae0 100644 --- a/web_platform/src/app/components/UrdfImportOptionsDialog.tsx +++ b/web_platform/src/app/components/UrdfImportOptionsDialog.tsx @@ -3,6 +3,7 @@ import { Camera, Settings2 } from 'lucide-react'; import type { CameraDirection, UrdfEnhancementOptions } from '../../project/urdfToMjcf'; import { Button, Dialog, Select, Tooltip } from '../../components/ui'; import { ROBOT_PROFILES } from '../../robot/registry'; +import { websiteCapabilities } from '../websiteConfig'; function OptionCard({ checked, @@ -56,7 +57,11 @@ export function UrdfImportOptionsDialog({ onConfirm: (options: UrdfEnhancementOptions, profileId?: string) => void; onSkip: () => void; }) { - const [profileId, setProfileId] = useState(initialProfileId); + const [profileId, setProfileId] = useState(() => + !websiteCapabilities.training && initialProfileId.startsWith('MobileManipulator-') + ? '' + : initialProfileId, + ); const [options, setOptions] = useState(() => ({ addActuators: true, addSensors: true, @@ -113,10 +118,14 @@ export function UrdfImportOptionsDialog({ onChange={(e) => setProfileId(e.target.value)} > - - + {websiteCapabilities.training && ( + <> + + + + )} {ROBOT_PROFILES.map((p) => (