可嵌入的 3D 资产创作 Web 应用与插件平台(MVP-1 基线)。
- 包边界:
@lumora/core(框架无关的插件运行时核心)、@lumora/studio(React 壳层 UI)、@lumora/plugin-sdk(插件作者公共 API) - 插件协议:
lumora.plugin.json(Manifest v1)+activate/deactivate生命周期 + 六类贡献项 - 嵌入方式:任意 React 应用以组件形式嵌入
LumoraStudio,监听 typed 事件,卸载即释放全部资源
npm install
npm run build # 构建全部包(core → plugin-sdk → studio → mock-plugin)
npm run dev # 启动示例嵌入宿主(http://localhost:5173)基线检查:
npm run lint # ESLint
npm run typecheck # tsc 全仓
npm run test # vitest 单元测试
npm run e2e:install # 首次安装 Playwright Chromium
npm run e2e # Playwright 端到端(自动起 dev server @5199)
npm run smoke:pack # 打包全部包,在临时工程以 tarball 安装并构建消费端(含 style.css 导入与 React 19 peer 边界)packages/core/ 插件运行时:Manifest 校验、引擎兼容、PluginHost 状态机、
CommandRegistry、ContributionRegistry、事件总线、服务门面
packages/plugin-sdk/ 插件作者 API:definePlugin / defineManifest / 贡献项类型
packages/studio/ React 壳层:LumoraStudio 组件、工具栏、面板宿主、
命令面板、插件管理、Three.js 场景视图
examples/mock-plugin/ 示例插件:六类贡献项全演示(lumora.plugin.json + 源码 + 测试)
examples/embedded-host/ 空白 React/Vite 宿主:嵌入 LumoraStudio、typed 事件日志、
挂载/卸载释放演示、非法/崩溃插件演示
e2e/ Playwright 基线:壳层渲染、事件、生命周期、错误隔离
import { useRef } from 'react';
import { LumoraStudio } from '@lumora/studio';
import '@lumora/studio/style.css'; // 壳层样式(构建产物为 dist/style.css)
import type { LumoraStudioHandle } from '@lumora/studio';
import type { PluginDescriptor } from '@lumora/core';
const plugins: PluginDescriptor[] = [
{
manifest: { /* lumora.plugin.json 内容 */ },
entry: () => import('my-plugin'),
},
];
function Host() {
const handle = useRef<LumoraStudioHandle>(null);
return (
<LumoraStudio
ref={handle}
plugins={plugins}
hostVersion="0.1.0"
initialProject={/* Project | undefined */}
/>
);
}- typed 事件:
handle.current.runtime.events.on('project:opened' | 'plugin:state-changed' | 'command:executed' | ...);自定义事件经onAny透传。 - 打开/关闭项目:
handle.current.runtime.openProject(project)/closeProject()。 - 卸载屏障(可等待):宿主卸载组件前必须先
await handle.current.close()—— 冲刷未保存变更后停用全部插件、移除全部订阅、销毁事件总线与 WebGL 场景(幂等;失败可重试)。返回{ ok: false }表示释放被拒绝(冲刷失败 / 存在未解决的恢复 fork),此时运行时未 teardown、组件必须保持挂载:先解决未保存内容(重试保存 / 另存副本 / 显式丢弃恢复快照)再重试close(),成功后才能真正卸载 UI。绝不「不等待直接卸载」或「假装已卸载」丢弃内容。 - 场景槽位:传入
scene={(project) => ...}可替换内置 Three.js 场景视图。
- 在插件目录写
lumora.plugin.json(Manifest v1):
{
"schemaVersion": "1",
"id": "com.example.my-plugin",
"name": "我的插件",
"version": "0.1.0",
"entry": "./dist/index.js",
"engine": { "lumora": ">=0.1.0" },
"contributes": ["panel", "command", "toolbar"]
}- 入口模块导出插件定义(
default或具名activate/deactivate均可):
import { definePlugin } from '@lumora/plugin-sdk';
export default definePlugin({
activate(context) {
// context: { pluginId, manifest, hostVersion, events, commands, services, contribute, getProject, log }
return context.contribute({
panels: [{ kind: 'panel', id: '...', title: '...', component: MyPanel }],
commands: [{ kind: 'command', command: { id: '...', title: '...', execute() { /* ... */ } } }],
toolbars: [{ kind: 'toolbar', id: '...', label: '...', commandId: '...' }],
assetLoaders: [{ kind: 'assetLoader', id: '...', extensions: ['.myext'], load(uri) { /* ... */ } }],
aiProviders: [{ kind: 'aiProvider', id: '...', models: ['m1'], chat(req) { /* AsyncIterable<string> */ } }],
exporters: [{ kind: 'exporter', id: '...', formats: ['json'], export(project) { /* ... */ } }],
});
},
deactivate() { /* 宿主已自动回收贡献项/命令/订阅,这里做自身清理 */ },
});- 状态机:
registered → loading → activating → active,任一步失败进入failed并记录明确原因;active可deactivate/disable后enable重新激活。 - 校验先于加载:Manifest 非法或引擎不兼容的插件不会加载入口模块(
entry不会被调用)。 - 三层错误隔离:激活异常(激活 try/catch)、命令异常(
execute返回CommandResult不抛出)、面板渲染异常(PanelErrorBoundary,可经边界直接禁用插件)。 - 资源代管:
contribute()的贡献项、context.events的订阅全部归入宿主代管集合,停用/卸载时统一释放;插件侧Disposable亦可提前释放。
core不依赖 React(面板组件类型为ComponentType,react作为 peer dep),便于未来非 React 宿主复用运行时。Manifest使用 zod 严格模式校验,未知字段直接拒绝;id要求小写反向域名风格。- 命令注册全局唯一;贡献项注册两阶段原子提交(整批校验 + 逆序回滚),失败不留下半注册状态。
- 命令上下文注入所属插件
pluginId,服务经惰性提供者解析(宿主构造完成后才冻结服务快照)。 - 插件激活为事务式:本次激活产生的资源暂存,成功后并入代管集合;失败或被并发停用取代时整体回滚,
failed插件可停用清理并可重新启用重试。 - 插件事件订阅经
TrackedEventBridge归入插件代管集合,停用时自动移除,宿主总线不泄漏。 - vitest 通过 alias 直连各包源码;tsconfig 以
paths做包间类型解析,构建时由各包独立打包。
- 应用内 npm 包下载 / 市场(marketplace)/ 支付 / 自动更新 —— 本期插件通过宿主显式传入
PluginDescriptor加载。 - Worker/iframe 沙箱:本期插件与宿主同进程运行,错误隔离靠状态机与边界组件实现;插件代码可信度要求高。已记录为架构演进项:未来将插件入口迁移到 Worker/iframe 沙箱以隔离任意代码执行。
- 宿主 peer 依赖声明为 React 19(
@lumora/studio/@lumora/core/@lumora/plugin-sdk均为^19.0.0),这是构建与 E2E 实际测试的版本边界;React 18 宿主不保证兼容。 - Playwright 断言使用真实 WebGL 渲染(headless 下由 Chromium SwiftShader 提供),场景内容以容器存在性验证为主。