feat: release v1.0.2 LeKiwi 语言控制与网站嵌入
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
集成服务器托管模型、自然语言移动与有界抓放、内置 LeKiwi URL 导入和双摄像头;同步部署契约与指定域名 iframe 白名单,保留原有物理安全、会话及调用预算防护。 更新 npm 包及锁文件版本、CHANGELOG 与发布文档。提交前 typecheck、120 项定向前端测试和 44 项后端测试通过(3 项可选跳过);真实 v2 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
This commit is contained in:
@@ -1,119 +1,53 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { resolve } from 'node:path';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { loadWebsiteRobot, runWebsiteCommand, readRobotResult } from './website.helpers';
|
||||
|
||||
const prefix = '/api/decision/v1';
|
||||
test('网站生产构建:精简设置、同源 Cookie/CSRF、双浏览器隔离与假 HTTP 上游', async ({
|
||||
page,
|
||||
browser,
|
||||
}, info) => {
|
||||
const errors: string[] = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveTitle(/CADWorld Sim/);
|
||||
await page
|
||||
.locator('input[type=file]')
|
||||
.first()
|
||||
.setInputFiles(resolve('web_platform/fixtures/python_controller/model.xml'));
|
||||
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 60000 });
|
||||
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 })).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await page.getByRole('button', { name: 'LeKiwi 智能抓放', exact: true }).click();
|
||||
await page.getByText('模型设置 · API_KEY / ChatGPT 订阅', { exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '保存设置' })).toBeEnabled();
|
||||
await expect(page.getByLabel('本机服务令牌')).toHaveCount(0);
|
||||
await page.getByLabel('LLM API_KEY', { exact: true }).fill('e2e-llm-fixture-not-real');
|
||||
await page.getByLabel('Jev API_KEY(OpenRouter)').fill('e2e-jev-fixture-not-real');
|
||||
await page.getByRole('button', { name: '保存设置', exact: true }).click();
|
||||
await expect(page.getByRole('status').filter({ hasText: '设置已保存' })).toBeVisible();
|
||||
await expect(page.getByLabel('LLM API_KEY', { exact: true })).toHaveValue('');
|
||||
await page.getByText('连接测试与凭据管理', { exact: true }).click();
|
||||
await page.getByRole('button', { name: '测试 LLM(可能计费)', exact: true }).click();
|
||||
await expect(page.getByText(/LLM 连接测试通过/)).toBeVisible();
|
||||
const other = await browser.newContext();
|
||||
try {
|
||||
const second = await other.newPage();
|
||||
await second.goto('/');
|
||||
const fresh = await second.evaluate(async (prefix) => {
|
||||
const response = await fetch(prefix + '/session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
});
|
||||
return response.json();
|
||||
}, prefix);
|
||||
expect(fresh.ready).toBe(false);
|
||||
const state = await page.evaluate(
|
||||
async (prefix) => (await fetch(prefix + '/status')).json(),
|
||||
prefix,
|
||||
);
|
||||
expect(state.ready).toBe(true);
|
||||
const forbidden = await page.evaluate(
|
||||
async (prefix) =>
|
||||
(
|
||||
await fetch(prefix + '/configuration', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
})
|
||||
).status,
|
||||
prefix,
|
||||
);
|
||||
expect(forbidden).toBe(403);
|
||||
expect(await page.evaluate(() => document.cookie)).not.toContain('cadworld');
|
||||
expect(
|
||||
await page.evaluate(() => JSON.stringify({ ...localStorage, ...sessionStorage })),
|
||||
).not.toContain('e2e-llm');
|
||||
} finally {
|
||||
await other.close();
|
||||
}
|
||||
await page.getByRole('button', { name: 'ChatGPT 订阅', exact: true }).click();
|
||||
await expect(page.getByRole('button', { name: '登录 ChatGPT 订阅', exact: true })).toBeVisible();
|
||||
await page.screenshot({ path: info.outputPath('website-settings.png') });
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
|
||||
test('网站 A 模型:真实 WASM、显式 mock 抓放和证据导出(无远端模型调用)', async ({
|
||||
test('服务器内置设置+假 HTTP 上游:不同语言改变真实 WASM 位移与角度,不重载机器人', async ({
|
||||
page,
|
||||
}, info) => {
|
||||
test.skip(!existsSync('build/lekiwi/lekiwi-v1.zip'), '需要本地 A 模型,不自动公开或下载资产');
|
||||
let calls = 0;
|
||||
page.on('request', (r) => {
|
||||
if (/\/api\/decision\/v1\/(plan|decide)$/.test(r.url())) calls++;
|
||||
});
|
||||
await page.goto('/');
|
||||
await page
|
||||
.locator('input[type=file]')
|
||||
.first()
|
||||
.setInputFiles(resolve('build/lekiwi/lekiwi-v1.zip'));
|
||||
const dialog = page.getByRole('dialog', { name: '配置 URDF 仿真组件' });
|
||||
await expect(dialog).toBeVisible({ timeout: 30000 });
|
||||
await dialog.getByLabel('URDF 机器人 profile').selectOption('lekiwi-v1');
|
||||
await dialog.getByRole('button', { name: '转换并加载' }).click();
|
||||
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeEnabled({ timeout: 60000 });
|
||||
await page.getByRole('tab', { name: '控制台', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'LeKiwi 智能抓放', exact: true }).click();
|
||||
const panel = page.getByLabel('LeKiwi 智能抓放任务', { exact: true });
|
||||
await panel.getByRole('button', { name: '开始任务(mock)', exact: true }).click();
|
||||
await expect(panel.getByTestId('agent-task-state')).toContainText('物理验收成功', {
|
||||
timeout: 180000,
|
||||
});
|
||||
await panel.getByText('高级操作与证据导出', { exact: true }).click();
|
||||
const downloaded = page.waitForEvent('download');
|
||||
await panel.getByRole('button', { name: '导出任务证据', exact: true }).click();
|
||||
const evidence = JSON.parse(await readFile((await (await downloaded).path())!, 'utf8'));
|
||||
expect(evidence.mode).toBe('mock');
|
||||
expect(evidence.succeeded).toBe(true);
|
||||
expect(evidence.transported).toBeGreaterThanOrEqual(0.5);
|
||||
expect(evidence.sample.onGoalSupport).toBe(true);
|
||||
expect(calls).toBe(0);
|
||||
await writeFile(
|
||||
info.outputPath('website-physical-evidence.json'),
|
||||
JSON.stringify(evidence, null, 2),
|
||||
);
|
||||
await page.screenshot({ path: info.outputPath('website-grasp.png') });
|
||||
const panel = await loadWebsiteRobot(page);
|
||||
const forward = await runWebsiteCommand(page, '前进0.5米', info, 'forward');
|
||||
expect(forward.distance).toBeGreaterThan(0.49);
|
||||
expect(forward.distance).toBeLessThan(0.51);
|
||||
const turn = await runWebsiteCommand(page, '左转90度', info, 'turn');
|
||||
expect(turn.turnDegrees).toBeGreaterThan(89);
|
||||
expect(turn.turnDegrees).toBeLessThan(91);
|
||||
expect(turn.start.x).toBeCloseTo(forward.sample.x, 2);
|
||||
const next = await runWebsiteCommand(page, '前进0.2米', info, 'forward-new-heading');
|
||||
expect(next.sample.y - next.start.y).toBeGreaterThan(0.19);
|
||||
const right = await runWebsiteCommand(page, '右转90度', info, 'turn-right');
|
||||
expect(right.turnDegrees).toBeGreaterThan(-91);
|
||||
expect(right.turnDegrees).toBeLessThan(-89);
|
||||
const back = await runWebsiteCommand(page, '后退0.2米', info, 'backward');
|
||||
expect(back.sample.x - back.start.x).toBeLessThan(-0.19);
|
||||
await panel.getByRole('textbox', { name: '机器人指令' }).fill('转动90度');
|
||||
await panel.getByRole('button', { name: '执行指令', exact: true }).click();
|
||||
await expect(panel.getByRole('status')).toContainText('请说明左转还是右转');
|
||||
await expect(panel.getByTestId('robot-command-state')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeVisible();
|
||||
});
|
||||
test('内置设置+假 HTTP 上游:保留完整单方块真实物理抓放,不手填密钥', async ({ page }, info) => {
|
||||
const panel = await loadWebsiteRobot(page);
|
||||
await panel.getByRole('button', { name: '载入预设场景' }).click();
|
||||
await expect(panel.getByRole('status')).toContainText('预设已载入', { timeout: 90000 });
|
||||
const data = await runWebsiteCommand(page, '把方块搬到 B 区', info, 'pick-place');
|
||||
expect(data.transported).toBeGreaterThanOrEqual(0.5);
|
||||
expect(data.sample.onGoalSupport).toBe(true);
|
||||
expect(data.receipts).toHaveLength(14);
|
||||
});
|
||||
test('正在运动时停止:撤销控制权并暂停物理,不把中断当成功', async ({ page }, info) => {
|
||||
const panel = await loadWebsiteRobot(page);
|
||||
await panel.getByRole('textbox', { name: '机器人指令' }).fill('前进0.5米');
|
||||
await panel.getByRole('button', { name: '执行指令', exact: true }).click();
|
||||
await expect(panel.getByTestId('robot-command-state')).toContainText('执行中', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await panel.getByRole('button', { name: '停止', exact: true }).click();
|
||||
await expect(panel.getByTestId('robot-command-state')).toContainText('已停止');
|
||||
await expect(page.getByRole('button', { name: '▶ 播放' })).toBeVisible();
|
||||
const data = await readRobotResult(page);
|
||||
expect(data.state).toBe('cancelled');
|
||||
expect(data.succeeded).toBe(false);
|
||||
expect(data.distance).toBeLessThan(0.1);
|
||||
await writeFile(info.outputPath('cancelled-motion.json'), JSON.stringify(data, null, 2));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user