refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构 (#216)

* refactor(editor): 提取行为树编辑器为独立包并重构编辑器架构

* feat(editor): 添加插件市场功能

* feat(editor): 重构插件市场以支持版本管理和ZIP打包

* feat(editor): 重构插件发布流程并修复React渲染警告

* fix(plugin): 修复插件发布和市场的路径不一致问题

* feat: 重构插件发布流程并添加插件删除功能

* fix(editor): 完善插件删除功能并修复多个关键问题

* fix(auth): 修复自动登录与手动登录的竞态条件问题

* feat(editor): 重构插件管理流程

* feat(editor): 支持 ZIP 文件直接发布插件

- 新增 PluginSourceParser 解析插件源
- 重构发布流程支持文件夹和 ZIP 两种方式
- 优化发布向导 UI

* feat(editor): 插件市场支持多版本安装

- 插件解压到项目 plugins 目录
- 新增 Tauri 后端安装/卸载命令
- 支持选择任意版本安装
- 修复打包逻辑,保留完整 dist 目录结构

* feat(editor): 个人中心支持多版本管理

- 合并同一插件的不同版本
- 添加版本历史展开/折叠功能
- 禁止有待审核 PR 时更新插件

* fix(editor): 修复 InspectorRegistry 服务注册

- InspectorRegistry 实现 IService 接口
- 注册到 Core.services 供插件使用

* feat(behavior-tree-editor): 完善插件注册和文件操作

- 添加文件创建模板和操作处理器
- 实现右键菜单创建行为树功能
- 修复文件读取权限问题(使用 Tauri 命令)
- 添加 BehaviorTreeEditorPanel 组件
- 修复 rollup 配置支持动态导入

* feat(plugin): 完善插件构建和发布流程

* fix(behavior-tree-editor): 完整恢复编辑器并修复 Toast 集成

* fix(behavior-tree-editor): 修复节点选中、连线跟随和文件加载问题并优化性能

* fix(behavior-tree-editor): 修复端口连接失败问题并优化连线样式

* refactor(behavior-tree-editor): 移除调试面板功能简化代码结构

* refactor(behavior-tree-editor): 清理冗余代码合并重复逻辑

* feat(behavior-tree-editor): 完善编辑器核心功能增强扩展性

* fix(lint): 修复ESLint错误确保CI通过

* refactor(behavior-tree-editor): 优化编辑器工具栏和编译器功能

* refactor(behavior-tree-editor): 清理技术债务,优化代码质量

* fix(editor-app): 修复字符串替换安全问题
This commit is contained in:
YHH
2025-11-18 14:46:51 +08:00
committed by GitHub
parent eac660b1a0
commit bce3a6e253
251 changed files with 26144 additions and 8844 deletions

View File

@@ -0,0 +1,354 @@
import { useState } from 'react';
import { X, FolderOpen, Loader, CheckCircle, AlertCircle, RefreshCw } from 'lucide-react';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import type { GitHubService, PublishedPlugin } from '../services/GitHubService';
import { PluginPublishService, type PublishProgress } from '../services/PluginPublishService';
import { PluginBuildService, type BuildProgress } from '../services/PluginBuildService';
import { open } from '@tauri-apps/plugin-shell';
import { EditorPluginCategory } from '@esengine/editor-core';
import type { IEditorPluginMetadata } from '@esengine/editor-core';
import '../styles/PluginUpdateDialog.css';
interface PluginUpdateDialogProps {
plugin: PublishedPlugin;
githubService: GitHubService;
onClose: () => void;
onSuccess: () => void;
locale: string;
}
type Step = 'selectFolder' | 'info' | 'building' | 'publishing' | 'success' | 'error';
function calculateNextVersion(currentVersion: string): string {
const parts = currentVersion.split('.').map(Number);
if (parts.length !== 3 || parts.some(isNaN)) return currentVersion;
const [major, minor, patch] = parts;
return `${major}.${minor}.${(patch ?? 0) + 1}`;
}
export function PluginUpdateDialog({ plugin, githubService, onClose, onSuccess, locale }: PluginUpdateDialogProps) {
const [publishService] = useState(() => new PluginPublishService(githubService));
const [buildService] = useState(() => new PluginBuildService());
const [step, setStep] = useState<Step>('selectFolder');
const [pluginFolder, setPluginFolder] = useState('');
const [version, setVersion] = useState('');
const [releaseNotes, setReleaseNotes] = useState('');
const [suggestedVersion] = useState(() => calculateNextVersion(plugin.latestVersion));
const [error, setError] = useState('');
const [buildLog, setBuildLog] = useState<string[]>([]);
const [buildProgress, setBuildProgress] = useState<BuildProgress | null>(null);
const [publishProgress, setPublishProgress] = useState<PublishProgress | null>(null);
const [prUrl, setPrUrl] = useState('');
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
zh: {
title: '更新插件',
currentVersion: '当前版本',
newVersion: '新版本号',
useSuggested: '使用建议版本',
releaseNotes: '更新说明',
releaseNotesPlaceholder: '描述这个版本的变更...',
selectFolder: '选择插件文件夹',
selectFolderDesc: '选择包含更新后插件源代码的文件夹',
browseFolder: '浏览文件夹',
selectedFolder: '已选择文件夹',
next: '下一步',
back: '上一步',
buildAndPublish: '构建并发布',
building: '构建中...',
publishing: '发布中...',
success: '更新成功!',
error: '更新失败',
viewPR: '查看 PR',
close: '关闭',
buildError: '构建失败',
publishError: '发布失败',
buildingStep1: '正在安装依赖...',
buildingStep2: '正在构建项目...',
buildingStep3: '正在打包 ZIP...',
publishingStep1: '正在 Fork 仓库...',
publishingStep2: '正在创建分支...',
publishingStep3: '正在上传文件...',
publishingStep4: '正在创建 Pull Request...',
reviewMessage: '你的插件更新已创建 PR维护者将进行审核。审核通过后新版本将自动发布到市场。'
},
en: {
title: 'Update Plugin',
currentVersion: 'Current Version',
newVersion: 'New Version',
useSuggested: 'Use Suggested',
releaseNotes: 'Release Notes',
releaseNotesPlaceholder: 'Describe the changes in this version...',
selectFolder: 'Select Plugin Folder',
selectFolderDesc: 'Select the folder containing your updated plugin source code',
browseFolder: 'Browse Folder',
selectedFolder: 'Selected Folder',
next: 'Next',
back: 'Back',
buildAndPublish: 'Build & Publish',
building: 'Building...',
publishing: 'Publishing...',
success: 'Update Successful!',
error: 'Update Failed',
viewPR: 'View PR',
close: 'Close',
buildError: 'Build Failed',
publishError: 'Publish Failed',
buildingStep1: 'Installing dependencies...',
buildingStep2: 'Building project...',
buildingStep3: 'Packaging ZIP...',
publishingStep1: 'Forking repository...',
publishingStep2: 'Creating branch...',
publishingStep3: 'Uploading files...',
publishingStep4: 'Creating Pull Request...',
reviewMessage: 'Your plugin update has been created as a PR. Maintainers will review it. Once approved, the new version will be published to the marketplace.'
}
};
return translations[locale]?.[key] || translations.en?.[key] || key;
};
const handleSelectFolder = async () => {
try {
const selected = await openDialog({
directory: true,
multiple: false,
title: t('selectFolder')
});
if (!selected) return;
setPluginFolder(selected as string);
setStep('info');
} catch (err) {
console.error('[PluginUpdateDialog] Failed to select folder:', err);
setError(err instanceof Error ? err.message : 'Failed to select folder');
}
};
const handleBuildAndPublish = async () => {
if (!version || !releaseNotes) {
alert('Please fill in all required fields');
return;
}
setStep('building');
setBuildLog([]);
setError('');
try {
buildService.setProgressCallback((progress) => {
setBuildProgress(progress);
if (progress.output) {
setBuildLog((prev) => [...prev, progress.output!]);
}
});
const zipPath = await buildService.buildPlugin(pluginFolder);
console.log('[PluginUpdateDialog] Build completed:', zipPath);
setStep('publishing');
publishService.setProgressCallback((progress) => {
setPublishProgress(progress);
});
const { readTextFile } = await import('@tauri-apps/plugin-fs');
const packageJsonPath = `${pluginFolder}/package.json`;
const packageJsonContent = await readTextFile(packageJsonPath);
const pkgJson = JSON.parse(packageJsonContent);
const pluginMetadata: IEditorPluginMetadata = {
name: pkgJson.name,
displayName: pkgJson.description || pkgJson.name,
description: pkgJson.description || '',
version: pkgJson.version,
category: EditorPluginCategory.Tool,
icon: 'Package',
enabled: true,
installedAt: Date.now()
};
const publishInfo = {
pluginMetadata,
version,
releaseNotes,
category: plugin.category_type as 'official' | 'community',
repositoryUrl: plugin.repositoryUrl || '',
tags: []
};
const prUrl = await publishService.publishPlugin(publishInfo, zipPath);
console.log('[PluginUpdateDialog] Update published:', prUrl);
setPrUrl(prUrl);
setStep('success');
onSuccess();
} catch (err) {
console.error('[PluginUpdateDialog] Failed to update plugin:', err);
setError(err instanceof Error ? err.message : 'Update failed');
setStep('error');
}
};
const renderStepContent = () => {
switch (step) {
case 'selectFolder':
return (
<div className="update-dialog-step">
<h3>{t('selectFolder')}</h3>
<p className="step-description">{t('selectFolderDesc')}</p>
<button className="btn-browse" onClick={handleSelectFolder}>
<FolderOpen size={16} />
{t('browseFolder')}
</button>
</div>
);
case 'info':
return (
<div className="update-dialog-step">
<div className="current-plugin-info">
<h4>{plugin.name}</h4>
<p>
{t('currentVersion')}: <strong>v{plugin.latestVersion}</strong>
</p>
</div>
{pluginFolder && (
<div className="selected-folder-info">
<FolderOpen size={16} />
<span>{pluginFolder}</span>
</div>
)}
<div className="form-group">
<label>{t('newVersion')} *</label>
<div className="version-input-group">
<input
type="text"
value={version}
onChange={(e) => setVersion(e.target.value)}
placeholder={suggestedVersion}
/>
<button
type="button"
className="btn-suggest"
onClick={() => setVersion(suggestedVersion)}
>
{t('useSuggested')} ({suggestedVersion})
</button>
</div>
</div>
<div className="form-group">
<label>{t('releaseNotes')} *</label>
<textarea
rows={6}
value={releaseNotes}
onChange={(e) => setReleaseNotes(e.target.value)}
placeholder={t('releaseNotesPlaceholder')}
/>
</div>
<div className="update-dialog-actions">
<button className="btn-back" onClick={() => setStep('selectFolder')}>
{t('back')}
</button>
<button
className="btn-primary"
onClick={handleBuildAndPublish}
disabled={!version || !releaseNotes}
>
{t('buildAndPublish')}
</button>
</div>
</div>
);
case 'building':
return (
<div className="update-dialog-step">
<h3>{t('building')}</h3>
{buildProgress && (
<div className="progress-container">
<p className="progress-message">{buildProgress.message}</p>
</div>
)}
{buildLog.length > 0 && (
<div className="build-log">
{buildLog.map((log, index) => (
<div key={index} className="log-line">
{log}
</div>
))}
</div>
)}
</div>
);
case 'publishing':
return (
<div className="update-dialog-step">
<h3>{t('publishing')}</h3>
{publishProgress && (
<div className="progress-container">
<div className="progress-bar">
<div className="progress-fill" style={{ width: `${publishProgress.progress}%` }} />
</div>
<p className="progress-message">{publishProgress.message}</p>
</div>
)}
</div>
);
case 'success':
return (
<div className="update-dialog-step success-step">
<CheckCircle size={64} className="success-icon" />
<h3>{t('success')}</h3>
<p className="success-message">{t('reviewMessage')}</p>
{prUrl && (
<button className="btn-view-pr" onClick={() => open(prUrl)}>
{t('viewPR')}
</button>
)}
<button className="btn-close" onClick={onClose}>
{t('close')}
</button>
</div>
);
case 'error':
return (
<div className="update-dialog-step error-step">
<AlertCircle size={64} className="error-icon" />
<h3>{t('error')}</h3>
<p className="error-message">{error}</p>
<button className="btn-close" onClick={onClose}>
{t('close')}
</button>
</div>
);
default:
return null;
}
};
return (
<div className="plugin-update-dialog-overlay">
<div className="plugin-update-dialog">
<div className="update-dialog-header">
<h2>{t('title')}: {plugin.name}</h2>
<button className="update-dialog-close" onClick={onClose}>
<X size={20} />
</button>
</div>
<div className="update-dialog-content">{renderStepContent()}</div>
</div>
</div>
);
}