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

集成服务器托管模型、自然语言移动与有界抓放、内置 LeKiwi URL 导入和双摄像头;同步部署契约与指定域名 iframe 白名单,保留原有物理安全、会话及调用预算防护。

更新 npm 包及锁文件版本、CHANGELOG 与发布文档。提交前 typecheck、120 项定向前端测试和 44 项后端测试通过(3 项可选跳过);真实 v2 云模型抓放仍待单独验收,不包含运行密钥或构建产物。
This commit is contained in:
2026-09-24 15:29:49 +08:00
parent f3a8a38acd
commit 0d986f60bd
93 changed files with 5166 additions and 1094 deletions
+101
View File
@@ -1,3 +1,10 @@
import {
composeLanguageScene,
LANGUAGE_SCENE,
resolveTarget,
} from '../src/mobile/agent/LanguageScene';
import { PickPlaceExecutor } from '../src/mobile/agent/PickPlaceExecutor';
import { LeKiwiIK } from '../src/mobile/agent/LeKiwiIK';
import { MainThreadPhysicsAdapter } from '../src/simulation/PhysicsAdapter';
import { importBrowserFiles } from '../src/project/importer';
import type { ProjectManifest } from '../src/project/types';
@@ -29,6 +36,100 @@ const api = {
if (!response.ok) throw new Error(`A 资产 HTTP ${response.status}`);
source = await importBrowserFiles([new File([await response.blob()], 'lekiwi-v1.zip')]);
},
async calibrateLanguage() {
if (!source) throw new Error('尚未导入 A 资产');
await adapter.load(source, 'URDF/LeKiwi.urdf', {
robotProfileId: 'lekiwi-v1',
configureRobotRuntime: false,
mobileRobot: PICK_PLACE_ROBOT,
urdfMode: 'mjcf',
baseMode: 'floating',
map: { kind: 'none' },
sceneComposer: (xml) => composeLanguageScene(xml, true),
});
adapter.releaseRetired();
const session = adapter.session!;
const ik = new LeKiwiIK(session);
const before = Array.from(session.data.qpos);
try {
const targets = [
LANGUAGE_SCENE.source,
...LANGUAGE_SCENE.targets.map((t) => t.position),
[0.237, 0.365, 0.128],
[0.277, 0.865, 0.128],
];
const checks = targets.flatMap((p) => {
resolveTarget('coordinates', p);
const pose = { x: p[0] - 0.257, y: p[1] - 0.015, yaw: 0 };
return [p[2], 0.195, 0.205, 0.223].map((z) => ({
target: [p[0], p[1], z],
residual: ik.solve([p[0], p[1], z], pose).residual,
}));
});
return { checks, unchanged: before.every((v, i) => session.data.qpos[i] === v) };
} finally {
ik.dispose();
}
},
async languagePhysical(goals: number[][], fault: false | 'stop' | 'empty' = false) {
await api.calibrateLanguage();
const session = adapter.session!;
const results = [];
for (const goal of goals) {
let skills!: PickPlaceExecutor;
let cancelled = false;
session.startAgent((write) => {
const grip = new ModelBindings(session.model).scalarActuator(
PICK_PLACE_ROBOT.gripperActuators[0].name,
PICK_PLACE_ROBOT.gripperJoint,
'position',
).actuatorId;
skills = new PickPlaceExecutor(
session,
(id, value) =>
write(id, fault === 'empty' && id === grip ? PICK_PLACE_ROBOT.gripperOpen : value),
resolveTarget('coordinates', goal),
false,
);
return {
beforeStep: () => {
if (cancelled) return false;
skills.beforeStep();
return skills.status === 'running';
},
afterStep: (dt) => skills.afterStep(dt),
hold: () => skills.hold(),
cancel: () => {
cancelled = true;
},
dispose: () => skills.dispose(),
};
});
session.setPaused(false);
let frames = 0;
while (skills.status === 'running' && !cancelled) {
session.singleStep();
if (++frames % 20 === 0) session.module.mj_forward(session.model, session.data);
if (fault === 'stop' && skills.phase === 'carry' && skills.evaluator.transported > 0.08)
session.stopAgent('测试运行中停止');
if (frames % 200 === 0) await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
results.push({
status: skills.status,
error: skills.error,
goal,
sample: skills.sample,
transported: skills.evaluator.transported,
succeeded: skills.evaluator.succeeded,
maxJointSpeed: skills.maxJointSpeed,
trace: skills.trace,
cancelled,
});
session.stopAgent('回合结束');
if (skills.status !== 'succeeded') break;
}
return results;
},
async baseline(seed: number, governed = false) {
const epoch = ++generation;
task?.dispose();