refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
// AI 编辑助手(editor-shell 预设插件贡献 ai-assistant 插槽,M3.8 注册;入口在编辑器顶部工具按钮浮层)。
|
||||
// M6.3:对话面板 —— 时间线摘要注入 system prompt,LLM 输出受 ai-command-schema 约束;
|
||||
// ≤3 条命令直执行,>3 条先预览确认(ADR-0007:AI 编辑交互=预设插件且命令受约束)。
|
||||
// 命令逐条 dispatch;宿主 fail-closed:无效命令被 registry 拒绝并写入 saveError(顶部提示条),
|
||||
// 面板汇报为中性“已提交”,执行结果以时间线实际变化为准。
|
||||
// 模型显式选择:不再盲信 config.model 默认值(历史默认可能是图像模型导致文本请求失败),
|
||||
// 面板顶部提供文本能力模型下拉,请求时同时覆写 config.model/textModel,避免默认模型错误。
|
||||
// 时间线摘要作为上下文发送给文本模型,模型输出必须通过 ai-command-schema 校验。
|
||||
// 小批命令直接进入统一 dispatch,大批命令先由用户确认;宿主拒绝无效命令并显示 saveError。
|
||||
// 请求显式携带用户选择的文本模型,不能沿用可能指向图片模型的全局默认值。
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { Bot, Loader2, RotateCcw, Send, ShieldAlert } from "lucide-react";
|
||||
import { requestImageQuestion } from "@/services/api/image";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// 资产导入(editor-shell 预设插件贡献 asset-ingest 插槽,M3.4)。
|
||||
// 展示项目资产库,按来源区分「本地上传」与「项目素材」;点击资产 → makeClipFromAsset
|
||||
// → dispatch addClip 加入时间线(添加到匹配 kind 的轨道末尾)。资产是
|
||||
// "仅时间线作用域"直连媒体(nodeId=asset:<id>)。导入链路:uploadResourceFile →
|
||||
@@ -111,7 +110,7 @@ function AssetThumb({ asset }: { asset: ProjectAsset }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** 尝试用 <video> 抓取视频首帧作为封面 dataURL;失败返回 null(保持图标占位)。 */
|
||||
/** 尝试用 <video> 抓取视频首帧作为封面 dataURL;失败时保留媒体类型图标。 */
|
||||
function useVideoPoster(url: string | null): string | null {
|
||||
const [poster, setPoster] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
// 导出(editor-shell 预设插件贡献 export-renderer 插槽,M3.7)。
|
||||
// M4.2:默认提交后端渲染任务(POST /timeline/renders,ffmpeg 服务端合成,
|
||||
// 产物落资源存储,ResultJSON 返回 resourceId 供内嵌预览/下载);
|
||||
// ffmpeg.wasm 本地导出保留为离线/降级路径。
|
||||
// §3.1 引用契约:缺失 nodeId 源(节点已删除)按计划跳过,成片不因悬空引用失败。
|
||||
// 默认由后端 ffmpeg 渲染并把产物写入资源存储;浏览器 ffmpeg.wasm 只承担离线降级。
|
||||
// 构建渲染计划时跳过已失去媒体来源的片段,避免悬空 nodeId 阻断其余有效片段导出。
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Download, Loader2, PackageOpen, Server } from "lucide-react";
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// 片段检查器(editor-shell 预设插件贡献 inspector 插槽,M3.3)。
|
||||
// 读取选中片段(store.selectedClipId),属性编辑经 setClipProperty 命令入队
|
||||
// (可撤销、可回放、进黄金文件语义);无选中时显示项目概览。
|
||||
// 视觉参照 Concat inspector:分组小标题 + 紧凑控件 + sunken 滑块,token 随主题联动。
|
||||
// 并进入统一撤销历史;检查器不直接修改 project,避免绕过命令校验和持久化。
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 预览监视器(editor-shell 预设插件贡献 preview-renderer 插槽,M3.2)。
|
||||
// 浏览器内近似预览:真实媒体帧(storageKey → resolveMediaUrl)+ 播放头 + 时间码。
|
||||
// 近似渲染与导出(M3.7 的 buildTimelineRenderPlan)共享同一条"片段→媒体"解析路径,
|
||||
// 但这里只做时序呈现,不承诺像素级预览——像素级交给导出任务(M4)。
|
||||
// 预览与 buildTimelineRenderPlan 共享“片段→媒体”解析规则,但浏览器预览只保证时序一致;
|
||||
// 最终画面、编码和滤镜结果以后端导出任务为准。
|
||||
//
|
||||
// 播放模型:时间线 transport(store.transportMs)与监视器共享。
|
||||
// - 本地 playbackRef 以 rAF 逐帧推进(60fps 时间码),节流回写 store(~80ms),
|
||||
@@ -461,7 +460,7 @@ export function EditorPreviewMonitor() {
|
||||
<div className="max-w-xs text-xs leading-relaxed text-white/70">
|
||||
{project && project.clips.length > 0
|
||||
? activeClip
|
||||
? "当前时间点片段无媒体源(占位/字幕/音频)"
|
||||
? "当前时间点没有可显示画面(字幕或音频片段)"
|
||||
: "播放头位于空白处"
|
||||
: "时间线暂无片段,导入素材后在此预览"}
|
||||
</div>
|
||||
|
||||
@@ -13,9 +13,9 @@ import { EditorAiAssistant } from "./editor-ai-assistant";
|
||||
const manifest: PluginManifestV2 = {
|
||||
apiVersion: "yingce.plugin/v2",
|
||||
id: "editor-shell",
|
||||
name: "剪辑工作台(壳)",
|
||||
name: "剪辑工作台",
|
||||
version: "0.1.0",
|
||||
description: "编辑器预设插件垂直切片:注册时间线面板插槽,验证 v2 插件插槽注册→渲染链路。",
|
||||
description: "注册时间线、预览、检查器、素材、字幕、转写、导出和 AI 编辑八个工作台插槽。",
|
||||
author: "影策团队",
|
||||
surfaces: ["fullscreen"],
|
||||
permissions: ["timeline.read", "timeline.command", "export.run"],
|
||||
@@ -42,55 +42,47 @@ export const editorShellPlugin: RegisteredPlugin = {
|
||||
|
||||
registerPlugin(editorShellPlugin);
|
||||
|
||||
// M2.4:插槽渲染函数 = 真实时间线面板(命令状态机 + 手势)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "timeline-panel",
|
||||
render: () => <EditorTimelinePanel />,
|
||||
});
|
||||
|
||||
// M3.2:插槽渲染函数 = 预览监视器(浏览器内近似预览 + 播放头)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "preview-renderer",
|
||||
render: () => <EditorPreviewMonitor />,
|
||||
});
|
||||
// M3.3:插槽渲染函数 = 片段检查器(属性编辑入队可撤销)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "inspector",
|
||||
render: () => <EditorInspector />,
|
||||
});
|
||||
|
||||
// M3.4:插槽渲染函数 = 素材库(点击资产 → addClip 入轨)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "asset-ingest",
|
||||
render: () => <EditorAssetIngest />,
|
||||
});
|
||||
|
||||
// M3.5:插槽渲染函数 = 字幕工具(SRT 导入导出 + 从节点重建字幕快照)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "subtitle-tool",
|
||||
render: () => <EditorSubtitleTools />,
|
||||
});
|
||||
|
||||
// M3.6:转写(mock ASR → 字幕轨道,M4 接后端任务客户端)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "transcription-provider",
|
||||
render: () => <EditorTranscription />,
|
||||
});
|
||||
|
||||
// M3.7:导出(渲染计划 + ffmpeg.wasm 降级,M4 接后端任务)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "export-renderer",
|
||||
render: () => <EditorExport />,
|
||||
});
|
||||
|
||||
// M3.8:AI 助手占位(M6 补实现 ai-command-schema + timeline-summary)。
|
||||
registerEditorSlot({
|
||||
pluginId: manifest.id,
|
||||
slot: "ai-assistant",
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// 字幕工具(editor-shell 预设插件贡献 subtitle-tool 插槽,M3.5)。
|
||||
// SRT 导入导出(复用 srt-parser)+ 「从节点重建字幕片段」命令(§3.1 快照契约:
|
||||
// SRT 导入导出(复用 srt-parser)+ 「从节点重建字幕片段」命令(快照契约:
|
||||
// 节点 subtitleEntries 权威 → 重建时间线字幕快照,替换过期快照,单向显式同步)。
|
||||
// AI 字幕高亮与关键词标注已在 M6 落地,入口在画布「字幕」弹窗
|
||||
//(canvas-subtitle-dialog.tsx,走 ai.text 权限门禁):对权威节点字幕条目生成
|
||||
// 高亮/关键词,并随字幕条目渲染。此处(时间线字幕工具)字幕片段只是快照,
|
||||
// 编辑器预览尚无字幕文本渲染层,高亮视觉无处挂载,故不在此重复接 AI;
|
||||
// 待 M4 数据源接入、预览可渲染字幕文本后,再将高亮打通到时间线片段。
|
||||
// 时间线字幕片段是节点字幕的只读快照;AI 高亮只写权威节点字幕。
|
||||
// 当前预览不渲染字幕高亮,因此本工具不重复发起 AI 请求,避免产生两份互相漂移的结果。
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// 时间线面板(editor-shell 预设插件贡献,M2.4 手势 echo 最小件)。
|
||||
// 渲染链路:store.project(当前命令状态)→ 轨道/片段绝对定位 → 拖拽手势
|
||||
// (moveClip / trimClip)经 previewGesture 逐帧预览、commitGesture 一次性入历史。
|
||||
// 手势数学与渲染同源(同一 pxPerMs),保证拖拽所见即所得。
|
||||
// 撤销/重做与保存状态移入宿主顶栏(Concat 主菜单区),本面板保留缩放与片段统计。
|
||||
// 撤销、重做和保存由编辑器宿主统一处理,本面板只负责时间线交互和可视状态。
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Eye, EyeOff, Film, Magnet, Maximize, MousePointer2, Music2, Plus, Scissors, Slice, Subtitles, Trash2, Volume2, VolumeX, X, ZoomIn, ZoomOut } from "lucide-react";
|
||||
@@ -42,7 +41,7 @@ const TOOL_FG_ACTIVE = "bg-[var(--director-dock-active-surface)] text-[var(--dir
|
||||
|
||||
type GestureMode = "move" | "trim-start" | "trim-end" | null;
|
||||
|
||||
// 与时间线相关的场景:playheadMs 是标尺交互的临时坐标,随缩放同步换算。
|
||||
// 工具模式只影响指针手势解释;时间线数据修改仍必须通过命令分发。
|
||||
type TimelineTool = "select" | "razor";
|
||||
|
||||
type GestureState = {
|
||||
@@ -300,7 +299,7 @@ export function EditorTimelinePanel() {
|
||||
{project.clips.length === 0 ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 text-[var(--director-dock-fg)]">
|
||||
<Scissors className="size-8" />
|
||||
<p className="text-sm">时间线暂无片段,从素材库拖入或后续接入画布节点</p>
|
||||
<p className="text-sm">从素材库加入素材后即可开始剪辑</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative min-w-full" style={{ width: contentWidth }}>
|
||||
@@ -399,7 +398,7 @@ function TimelineRuler({
|
||||
};
|
||||
return (
|
||||
<div className="sticky top-0 z-10 flex h-6 shrink-0 w-full items-end border-b border-[var(--director-sequencer-border)] bg-[var(--director-sequencer-surface-raised)]">
|
||||
{/* 左列占位与 TrackRow 轨道标签列(w-48)同宽,sticky 跟随横向滚动,保证 0ms 刻度与片段区起点对齐 */}
|
||||
{/* 左侧空白列与轨道标签列同宽,保证 0ms 刻度和片段绘制区域使用同一坐标原点。 */}
|
||||
<div className="sticky left-0 z-10 w-48 shrink-0 self-stretch border-r border-[var(--director-sequencer-border)] bg-[var(--director-sequencer-surface-raised)]">
|
||||
{/* 当前播放头时码,跟随拖动实时刷新 */}
|
||||
<span className="absolute bottom-1 right-2 text-[10px] tabular-nums text-[var(--director-danger)]">{formatTimelineTime(playheadMs)}</span>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// 字幕转写(editor-shell 预设插件贡献 transcription-provider 插槽,M3.6)。
|
||||
// M4 后端转写任务(whisper.cpp 本地执行):创建 POST /timeline/transcriptions
|
||||
// → waitForGenerationTask 轮询进度 → 结果 segments 按 ADR-0004 协议以
|
||||
// SrtEntry[] 落字幕轨道(rebuildSubtitleClips 重建快照),UI 契约与 M3.6 mock 一致。
|
||||
// 转写由后端 whisper.cpp 任务执行;前端只提交资源 ID、观察任务状态,
|
||||
// 并将成功结果转换为 SrtEntry[],通过 rebuildSubtitleClips 原子替换字幕轨道快照。
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AudioLines, Loader2 } from "lucide-react";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// 编辑器预设插件集合入口(M3.8)。
|
||||
// editor-shell 单个预设插件贡献全部 8 种 editorSlots;此处聚合导出,
|
||||
// builtin/index.ts 的静态注册经 editor-shell 模块副作用完成。
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// 编辑器插件执行期权限校验(M5,fail-closed)。
|
||||
// v2 插件贡献 editorSlots / 代发 timeline 命令时,其 manifest.permissions 必须覆盖
|
||||
// 对应能力域;缺失即拒绝渲染/执行。v1 插件不贡献 editorSlots,天然不受此域约束。
|
||||
// 本模块只做纯判定,接入点在插槽渲染(SlotStack)与宿主命令代发通道。
|
||||
// 编辑器插件执行期权限校验,默认拒绝未知插件和缺失权限。
|
||||
// v2 插件贡献 editorSlots 或代发 timeline 命令时,manifest.permissions 必须覆盖对应能力域;
|
||||
// v1 插件不贡献 editorSlots,因此不经过这条插槽权限路径。本模块只做纯判定,
|
||||
// 接入点分别位于插槽渲染(SlotStack)和宿主命令代发通道。
|
||||
|
||||
import { getRegisteredPlugin } from "@/lib/plugins/plugin-registry";
|
||||
import type { EditorPluginPermission, EditorSlotKind } from "@/lib/plugins/plugin-types";
|
||||
|
||||
/** 每个编辑器插槽所需的最小权限域(M5.2 表驱动:新插槽必须补表,否则 fail-closed 拒渲染)。 */
|
||||
/** 每个编辑器插槽所需的最小权限域;新增插槽必须补表,否则会因找不到权限而拒绝渲染。 */
|
||||
export const EDITOR_SLOT_REQUIRED_PERMISSION: Record<EditorSlotKind, EditorPluginPermission> = {
|
||||
"timeline-panel": "timeline.command",
|
||||
"preview-renderer": "timeline.read",
|
||||
@@ -51,7 +51,7 @@ export function pluginMayRenderEditorSlot(pluginId: string, slot: EditorSlotKind
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/** 宿主命令代发通道的权限断言:插件未声明 permission 时抛错(M6 AI 命令、第三方代发复用)。 */
|
||||
/** 宿主命令代发通道的权限断言:未注册插件或未声明目标权限时抛错,调用方必须阻止命令执行。 */
|
||||
export function assertEditorPermission(pluginId: string, permission: EditorPluginPermission): void {
|
||||
const permissions = pluginPermissions(pluginId);
|
||||
if (permissions === null) {
|
||||
|
||||
Reference in New Issue
Block a user