Feature/runtime cdn and plugin loader (#240)

* feat(ui): 完善 UI 布局系统和编辑器可视化工具

* refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统

* fix: 修复 CodeQL 警告并提升测试覆盖率

* refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题

* fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤

* docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明

* fix(ci): 修复 type-check 失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖

* fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖

* fix(ci): platform-web 添加缺失的 behavior-tree 依赖

* fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
YHH
2025-11-27 20:42:46 +08:00
committed by GitHub
parent 71869b1a58
commit 107439d70c
367 changed files with 10661 additions and 12473 deletions

View File

@@ -1,142 +0,0 @@
import { createLogger } from '@esengine/ecs-framework';
const logger = createLogger('EditorEventBus');
type EventHandler<T = any> = (data: T) => void;
interface Subscription {
unsubscribe: () => void;
}
export enum EditorEvent {
NODE_CREATED = 'node:created',
NODE_DELETED = 'node:deleted',
NODE_UPDATED = 'node:updated',
NODE_MOVED = 'node:moved',
NODE_SELECTED = 'node:selected',
CONNECTION_ADDED = 'connection:added',
CONNECTION_REMOVED = 'connection:removed',
EXECUTION_STARTED = 'execution:started',
EXECUTION_PAUSED = 'execution:paused',
EXECUTION_RESUMED = 'execution:resumed',
EXECUTION_STOPPED = 'execution:stopped',
EXECUTION_STEPPED = 'execution:stepped',
EXECUTION_TICK = 'execution:tick',
EXECUTION_NODE_STATUS_CHANGED = 'execution:node_status_changed',
TREE_SAVED = 'tree:saved',
TREE_LOADED = 'tree:loaded',
TREE_VALIDATED = 'tree:validated',
BLACKBOARD_VARIABLE_UPDATED = 'blackboard:variable_updated',
BLACKBOARD_RESTORED = 'blackboard:restored',
CANVAS_ZOOM_CHANGED = 'canvas:zoom_changed',
CANVAS_PAN_CHANGED = 'canvas:pan_changed',
CANVAS_RESET = 'canvas:reset',
COMMAND_EXECUTED = 'command:executed',
COMMAND_UNDONE = 'command:undone',
COMMAND_REDONE = 'command:redone'
}
export class EditorEventBus {
private listeners: Map<string, Set<EventHandler>> = new Map();
private eventHistory: Array<{ event: string; data: any; timestamp: number }> = [];
private maxHistorySize: number = 100;
on<T = any>(event: EditorEvent | string, handler: EventHandler<T>): Subscription {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(handler);
return {
unsubscribe: () => this.off(event, handler)
};
}
once<T = any>(event: EditorEvent | string, handler: EventHandler<T>): Subscription {
const wrappedHandler = (data: T) => {
handler(data);
this.off(event, wrappedHandler);
};
return this.on(event, wrappedHandler);
}
off<T = any>(event: EditorEvent | string, handler: EventHandler<T>): void {
const handlers = this.listeners.get(event);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.listeners.delete(event);
}
}
}
emit<T = any>(event: EditorEvent | string, data?: T): void {
if (this.eventHistory.length >= this.maxHistorySize) {
this.eventHistory.shift();
}
this.eventHistory.push({
event,
data,
timestamp: Date.now()
});
const handlers = this.listeners.get(event);
if (handlers) {
handlers.forEach((handler) => {
try {
handler(data);
} catch (error) {
logger.error(`Error in event handler for ${event}:`, error);
}
});
}
}
clear(event?: EditorEvent | string): void {
if (event) {
this.listeners.delete(event);
} else {
this.listeners.clear();
}
}
getListenerCount(event: EditorEvent | string): number {
return this.listeners.get(event)?.size || 0;
}
getAllEvents(): string[] {
return Array.from(this.listeners.keys());
}
getEventHistory(count?: number): Array<{ event: string; data: any; timestamp: number }> {
if (count) {
return this.eventHistory.slice(-count);
}
return [...this.eventHistory];
}
clearHistory(): void {
this.eventHistory = [];
}
}
let globalEventBus: EditorEventBus | null = null;
export function getGlobalEventBus(): EditorEventBus {
if (!globalEventBus) {
globalEventBus = new EditorEventBus();
}
return globalEventBus;
}
export function resetGlobalEventBus(): void {
globalEventBus = null;
}

View File

@@ -1,97 +0,0 @@
import { NodeTemplate, NodeTemplates } from '@esengine/behavior-tree';
import { Node } from '../../domain/models/Node';
import { Position } from '../../domain/value-objects/Position';
import { INodeFactory } from '../../domain/interfaces/INodeFactory';
import { NodeRegistryService } from '../services/NodeRegistryService';
/**
* 生成唯一ID
*/
function generateUniqueId(): string {
return `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 节点工厂实现
*/
export class NodeFactory implements INodeFactory {
/**
* 创建节点
*/
createNode(
template: NodeTemplate,
position: Position,
data?: Record<string, unknown>
): Node {
const nodeId = generateUniqueId();
const nodeData = {
...template.defaultConfig,
...data
};
return new Node(nodeId, template, nodeData, position, []);
}
/**
* 根据模板类型创建节点
*/
createNodeByType(
nodeType: string,
position: Position,
data?: Record<string, unknown>
): Node {
const template = this.getTemplateByType(nodeType);
if (!template) {
throw new Error(`未找到节点模板: ${nodeType}`);
}
return this.createNode(template, position, data);
}
/**
* 克隆节点
*/
cloneNode(node: Node, newPosition?: Position): Node {
const position = newPosition || node.position;
const clonedId = generateUniqueId();
return new Node(
clonedId,
node.template,
node.data,
position,
[]
);
}
/**
* 获取所有可用的节点模板
*/
getAllTemplates(): NodeTemplate[] {
const coreTemplates = NodeTemplates.getAllTemplates();
const customTemplates = NodeRegistryService.getInstance().getCustomTemplates();
return [...coreTemplates, ...customTemplates];
}
/**
* 根据类型获取模板
*/
private getTemplateByType(nodeType: string): NodeTemplate | null {
const allTemplates = this.getAllTemplates();
const template = allTemplates.find((t: NodeTemplate) => {
const defaultNodeType = t.defaultConfig.nodeType;
return defaultNodeType === nodeType;
});
return template || null;
}
/**
* 根据实现类型获取模板
*/
getTemplateByImplementationType(implementationType: string): NodeTemplate | null {
const allTemplates = this.getAllTemplates();
return allTemplates.find((t) => t.className === implementationType) || null;
}
}

View File

@@ -1 +0,0 @@
export { NodeFactory } from './NodeFactory';

View File

@@ -1,2 +0,0 @@
export * from './factories';
export * from './serialization';

View File

@@ -1,127 +0,0 @@
import { BehaviorTree } from '../../domain/models/BehaviorTree';
import { ISerializer, SerializationFormat } from '../../domain/interfaces/ISerializer';
import { BehaviorTreeAssetSerializer, EditorFormatConverter } from '@esengine/behavior-tree';
/**
* 序列化选项
*/
export interface SerializationOptions {
/**
* 资产版本号
*/
version?: string;
/**
* 资产名称
*/
name?: string;
/**
* 资产描述
*/
description?: string;
/**
* 创建时间
*/
createdAt?: string;
/**
* 修改时间
*/
modifiedAt?: string;
}
/**
* 行为树序列化器实现
*/
export class BehaviorTreeSerializer implements ISerializer {
private readonly defaultOptions: Required<SerializationOptions> = {
version: '1.0.0',
name: 'Untitled Behavior Tree',
description: '',
createdAt: new Date().toISOString(),
modifiedAt: new Date().toISOString()
};
constructor(private readonly options: SerializationOptions = {}) {
this.defaultOptions = { ...this.defaultOptions, ...options };
}
/**
* 序列化行为树
*/
serialize(tree: BehaviorTree, format: SerializationFormat): string | Uint8Array {
const treeObject = tree.toObject();
if (format === 'json') {
return JSON.stringify(treeObject, null, 2);
}
throw new Error(`不支持的序列化格式: ${format}`);
}
/**
* 反序列化行为树
*/
deserialize(data: string | Uint8Array, format: SerializationFormat): BehaviorTree {
if (format === 'json') {
if (typeof data !== 'string') {
throw new Error('JSON 格式需要字符串数据');
}
const obj = JSON.parse(data);
return BehaviorTree.fromObject(obj);
}
throw new Error(`不支持的反序列化格式: ${format}`);
}
/**
* 导出为运行时资产格式
* @param tree 行为树
* @param format 导出格式
* @param options 可选的序列化选项(覆盖默认值)
*/
exportToRuntimeAsset(
tree: BehaviorTree,
format: SerializationFormat,
options?: SerializationOptions
): string | Uint8Array {
const nodes = tree.nodes.map((node) => ({
id: node.id,
template: node.template,
data: node.data,
position: node.position.toObject(),
children: Array.from(node.children)
}));
const connections = tree.connections.map((conn) => conn.toObject());
const blackboard = tree.blackboard.toObject();
const finalOptions = { ...this.defaultOptions, ...options };
finalOptions.modifiedAt = new Date().toISOString();
const editorFormat = {
version: finalOptions.version,
metadata: {
name: finalOptions.name,
description: finalOptions.description,
createdAt: finalOptions.createdAt,
modifiedAt: finalOptions.modifiedAt
},
nodes,
connections,
blackboard
};
const asset = EditorFormatConverter.toAsset(editorFormat);
if (format === 'json') {
return BehaviorTreeAssetSerializer.serialize(asset, { format: 'json', pretty: true });
} else if (format === 'binary') {
return BehaviorTreeAssetSerializer.serialize(asset, { format: 'binary' });
}
throw new Error(`不支持的导出格式: ${format}`);
}
}

View File

@@ -1 +0,0 @@
export { BehaviorTreeSerializer } from './BehaviorTreeSerializer';

View File

@@ -1,264 +0,0 @@
import { NodeTemplate, NodeMetadataRegistry, NodeMetadata, NodeType } from '@esengine/behavior-tree';
/**
* 简化的节点注册配置
*/
export interface NodeRegistrationConfig {
type: 'composite' | 'decorator' | 'action' | 'condition';
implementationType: string;
displayName: string;
description?: string;
category?: string;
icon?: string;
color?: string;
properties?: NodePropertyConfig[];
minChildren?: number;
maxChildren?: number;
}
/**
* 节点属性配置
*/
export interface NodePropertyConfig {
name: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'blackboard' | 'code';
label: string;
description?: string;
defaultValue?: any;
options?: Array<{ label: string; value: any }>;
min?: number;
max?: number;
required?: boolean;
}
/**
* 节点注册服务
* 提供编辑器级别的节点注册和管理功能
*/
export class NodeRegistryService {
private static instance: NodeRegistryService;
private customTemplates: Map<string, NodeTemplate> = new Map();
private registrationCallbacks: Array<(template: NodeTemplate) => void> = [];
private constructor() {}
static getInstance(): NodeRegistryService {
if (!this.instance) {
this.instance = new NodeRegistryService();
}
return this.instance;
}
/**
* 注册自定义节点类型
*/
registerNode(config: NodeRegistrationConfig): void {
const nodeType = this.mapStringToNodeType(config.type);
const metadata: NodeMetadata = {
implementationType: config.implementationType,
nodeType: nodeType,
displayName: config.displayName,
description: config.description || '',
category: config.category || this.getDefaultCategory(config.type),
configSchema: this.convertPropertiesToSchema(config.properties || []),
childrenConstraints: this.getChildrenConstraints(config)
};
class DummyExecutor {}
NodeMetadataRegistry.register(DummyExecutor, metadata);
const template = this.createTemplate(config, metadata);
this.customTemplates.set(config.implementationType, template);
this.registrationCallbacks.forEach((cb) => cb(template));
}
/**
* 注销节点类型
*/
unregisterNode(implementationType: string): boolean {
return this.customTemplates.delete(implementationType);
}
/**
* 获取所有自定义模板
*/
getCustomTemplates(): NodeTemplate[] {
return Array.from(this.customTemplates.values());
}
/**
* 检查节点类型是否已注册
*/
hasNode(implementationType: string): boolean {
return this.customTemplates.has(implementationType) ||
NodeMetadataRegistry.getMetadata(implementationType) !== undefined;
}
/**
* 监听节点注册事件
*/
onNodeRegistered(callback: (template: NodeTemplate) => void): () => void {
this.registrationCallbacks.push(callback);
return () => {
const index = this.registrationCallbacks.indexOf(callback);
if (index > -1) {
this.registrationCallbacks.splice(index, 1);
}
};
}
private mapStringToNodeType(type: string): NodeType {
switch (type) {
case 'composite': return NodeType.Composite;
case 'decorator': return NodeType.Decorator;
case 'action': return NodeType.Action;
case 'condition': return NodeType.Condition;
default: return NodeType.Action;
}
}
private getDefaultCategory(type: string): string {
switch (type) {
case 'composite': return '组合';
case 'decorator': return '装饰器';
case 'action': return '动作';
case 'condition': return '条件';
default: return '其他';
}
}
private convertPropertiesToSchema(properties: NodePropertyConfig[]): Record<string, any> {
const schema: Record<string, any> = {};
for (const prop of properties) {
schema[prop.name] = {
type: this.mapPropertyType(prop.type),
default: prop.defaultValue,
description: prop.description,
min: prop.min,
max: prop.max,
options: prop.options?.map((o) => o.value)
};
}
return schema;
}
private mapPropertyType(type: string): string {
switch (type) {
case 'string':
case 'code':
case 'blackboard':
case 'select':
return 'string';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
default:
return 'string';
}
}
private getChildrenConstraints(config: NodeRegistrationConfig): { min?: number; max?: number } | undefined {
if (config.minChildren !== undefined || config.maxChildren !== undefined) {
return {
min: config.minChildren,
max: config.maxChildren
};
}
switch (config.type) {
case 'composite':
return { min: 1 };
case 'decorator':
return { min: 1, max: 1 };
case 'action':
case 'condition':
return { max: 0 };
default:
return undefined;
}
}
private createTemplate(config: NodeRegistrationConfig, metadata: NodeMetadata): NodeTemplate {
const defaultConfig: any = {
nodeType: config.type
};
switch (config.type) {
case 'composite':
defaultConfig.compositeType = config.implementationType;
break;
case 'decorator':
defaultConfig.decoratorType = config.implementationType;
break;
case 'action':
defaultConfig.actionType = config.implementationType;
break;
case 'condition':
defaultConfig.conditionType = config.implementationType;
break;
}
for (const prop of config.properties || []) {
if (prop.defaultValue !== undefined) {
defaultConfig[prop.name] = prop.defaultValue;
}
}
const template: NodeTemplate = {
type: metadata.nodeType,
displayName: config.displayName,
category: config.category || this.getDefaultCategory(config.type),
description: config.description || '',
icon: config.icon || this.getDefaultIcon(config.type),
color: config.color || this.getDefaultColor(config.type),
className: config.implementationType,
defaultConfig,
properties: (config.properties || []).map((p) => ({
name: p.name,
type: p.type,
label: p.label,
description: p.description,
defaultValue: p.defaultValue,
options: p.options,
min: p.min,
max: p.max,
required: p.required
}))
};
if (config.minChildren !== undefined) {
template.minChildren = config.minChildren;
template.requiresChildren = config.minChildren > 0;
}
if (config.maxChildren !== undefined) {
template.maxChildren = config.maxChildren;
}
return template;
}
private getDefaultIcon(type: string): string {
switch (type) {
case 'composite': return 'GitBranch';
case 'decorator': return 'Settings';
case 'action': return 'Play';
case 'condition': return 'HelpCircle';
default: return 'Circle';
}
}
private getDefaultColor(type: string): string {
switch (type) {
case 'composite': return '#1976d2';
case 'decorator': return '#fb8c00';
case 'action': return '#388e3c';
case 'condition': return '#d32f2f';
default: return '#757575';
}
}
}

View File

@@ -1,149 +0,0 @@
import { IValidator, ValidationResult, ValidationError } from '../../domain/interfaces/IValidator';
import { BehaviorTree } from '../../domain/models/BehaviorTree';
import { Node } from '../../domain/models/Node';
import { Connection } from '../../domain/models/Connection';
/**
* 行为树验证器实现
*/
export class BehaviorTreeValidator implements IValidator {
/**
* 验证整个行为树
*/
validateTree(tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
// 验证所有节点
for (const node of tree.nodes) {
const nodeResult = this.validateNode(node);
errors.push(...nodeResult.errors);
}
// 验证所有连接
for (const connection of tree.connections) {
const connResult = this.validateConnection(connection, tree);
errors.push(...connResult.errors);
}
// 验证循环引用
const cycleResult = this.validateNoCycles(tree);
errors.push(...cycleResult.errors);
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证节点
*/
validateNode(node: Node): ValidationResult {
const errors: ValidationError[] = [];
// 验证节点必填字段
if (!node.id) {
errors.push({
message: '节点 ID 不能为空',
nodeId: node.id
});
}
if (!node.template) {
errors.push({
message: '节点模板不能为空',
nodeId: node.id
});
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证连接
*/
validateConnection(connection: Connection, tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
// 验证连接的源节点和目标节点都存在
const fromNode = tree.nodes.find((n) => n.id === connection.from);
const toNode = tree.nodes.find((n) => n.id === connection.to);
if (!fromNode) {
errors.push({
message: `连接的源节点不存在: ${connection.from}`
});
}
if (!toNode) {
errors.push({
message: `连接的目标节点不存在: ${connection.to}`
});
}
// 不能自己连接自己
if (connection.from === connection.to) {
errors.push({
message: '节点不能连接到自己',
nodeId: connection.from
});
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* 验证是否会产生循环引用
*/
validateNoCycles(tree: BehaviorTree): ValidationResult {
const errors: ValidationError[] = [];
const visited = new Set<string>();
const recursionStack = new Set<string>();
const hasCycle = (nodeId: string): boolean => {
if (recursionStack.has(nodeId)) {
return true;
}
if (visited.has(nodeId)) {
return false;
}
visited.add(nodeId);
recursionStack.add(nodeId);
const node = tree.nodes.find((n) => n.id === nodeId);
if (node) {
for (const childId of node.children) {
if (hasCycle(childId)) {
return true;
}
}
}
recursionStack.delete(nodeId);
return false;
};
for (const node of tree.nodes) {
if (hasCycle(node.id)) {
errors.push({
message: '行为树中存在循环引用',
nodeId: node.id
});
break;
}
}
return {
isValid: errors.length === 0,
errors
};
}
}