feat: release v1.0.1 CADWorld 网站与 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) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled

集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
This commit is contained in:
2026-09-24 09:57:41 +08:00
parent 3ad29356c9
commit f3a8a38acd
194 changed files with 32918 additions and 236 deletions
+87
View File
@@ -1,6 +1,9 @@
import { MainThreadPhysicsAdapter } from '../src/simulation/PhysicsAdapter';
import { SimulationSession } from '../src/simulation/SimulationSession';
import { ModelBindings } from '../src/simulation/ModelBindings';
import { mergeCachedFiles } from '../src/project/cachedFiles';
import { compiledProjectPackage } from '../src/project/exportPackage';
import { importBrowserFiles } from '../src/project/importer';
import { LEKIWI_PROFILE as p, bodyToWheels } from '../src/robot/profiles/lekiwi';
import type { ProjectManifest } from '../src/project/types';
import { ExternalControlClient } from '../src/robot/ExternalControlClient';
@@ -13,6 +16,7 @@ import { surfaceDistance, type Surface } from './meshDistance';
const adapter = new MainThreadPhysicsAdapter();
let bindings: ModelBindings;
let currentManifest: ProjectManifest;
let roundtripPackage: ReturnType<typeof compiledProjectPackage> | undefined;
let frame: number | undefined;
let bridge: ExternalControlClient | undefined;
function session() {
@@ -169,6 +173,7 @@ const harness = {
bridge?.sync();
},
async boot(assetBase: string) {
roundtripPackage = undefined;
const response = await fetch(`${assetBase}/source-manifest.json`);
if (!response.ok) throw new Error('Run examples/lekiwi/prepare_assets.py first');
const source = (await response.json()) as { files: Record<string, string> };
@@ -198,6 +203,8 @@ const harness = {
map: { kind: 'none' },
enhancements: { addActuators: false, addSensors: false, sensorType: 'camera' },
});
currentManifest = mergeCachedFiles(currentManifest, adapter.cachedSupportFiles());
adapter.releaseRetired();
bindings = new ModelBindings(session().model);
return { version: session().module.mj_versionString(), ...read() };
},
@@ -778,6 +785,86 @@ const harness = {
cancel: await cancel,
};
},
visualStats() {
const s = session(),
m = s.model,
d = s.data;
s.module.mj_forward(m, d);
const wheels = p.wheels.map((name) => {
const wheelBody = bindings.resolve(bindings.bodies, `__lekiwi_${name}`);
for (let id = 0; id < m.ngeom; id++) {
const geom = m.geom(id);
try {
if (
!geom.name.startsWith('4-Omni-Directional-Wheel_Single_Body') ||
Number(m.geom_group[id]) !== 1
)
continue;
let body = Number(m.geom_bodyid[id]);
while (body && body !== wheelBody) body = Number(m.body_parentid[body]);
if (body !== wheelBody) continue;
const meshId = Number(m.geom_dataid[id]);
const centerError = Math.hypot(
...[0, 1, 2].map((i) => d.geom_xpos[id * 3 + i] - d.xpos[wheelBody * 3 + i]),
);
return {
name,
faces: Number(m.mesh_facenum[meshId]),
vertices: Number(m.mesh_vertnum[meshId]),
scale: Array.from(m.mesh_scale.slice(meshId * 3, meshId * 3 + 3)),
centerError,
rotation: Array.from(d.geom_xmat.slice(id * 9, id * 9 + 9), Number),
collision: Boolean(m.geom_contype[id] || m.geom_conaffinity[id]),
mass: Number(m.body_mass[Number(m.geom_bodyid[id])]),
};
} finally {
geom.delete();
}
}
throw new Error(`missing CAD wheel ${name}`);
});
const fs = s.module as unknown as { FS: { readdir(path: string): string[] } };
return {
wheels,
heap: (d.qpos as Float64Array).buffer.byteLength,
projectBytes: currentManifest.files.reduce((sum, file) => sum + file.data.byteLength, 0),
xmlFiles: currentManifest.files
.filter((f) => f.path.endsWith('.xml'))
.map((f) => ({ path: f.path, bytes: f.size })),
workspaces: fs.FS.readdir('/workspace').filter((name) => name !== '.' && name !== '..'),
generatedMeshes: currentManifest.files
.filter((f) => /stl-obj-v1-.*\.obj$/.test(f.path))
.map((f) => ({ path: f.path, bytes: f.size })),
};
},
async packageRoundtrip() {
// Reload the SAME exported package. Re-exporting into the imported sources
// would intentionally append a new XML snapshot each round, not test leaks.
const packaged = (roundtripPackage ??= compiledProjectPackage(
currentManifest,
'URDF/LeKiwi.urdf',
adapter.exportMjcf(),
adapter.cachedSupportFiles(),
));
const imported = await importBrowserFiles([
new File([packaged.data.slice().buffer], 'complete.zip'),
]);
const old = session();
await adapter.load(imported, packaged.entryPath, {
robotProfileId: p.id,
urdfMode: 'mjcf',
baseMode: 'floating',
map: { kind: 'none' },
});
adapter.releaseRetired();
currentManifest = imported;
bindings = new ModelBindings(session().model);
return {
...harness.visualStats(),
oldModelDeleted: old.model.isDeleted(),
oldDataDeleted: old.data.isDeleted(),
};
},
async reimport() {
const data = adapter.exportMjcf();
const file = {