From 0861d1b1ec569fb4f017236543ecc49e94068986 Mon Sep 17 00:00:00 2001 From: KKKK Date: Thu, 22 Oct 2026 03:04:24 +0800 Subject: [PATCH 1/4] feat: add persistent project organization and harden local runtime Adapt the organization and group-layout work from JaysonAlbert's fork PR #16. Keep official providers, pinned assets and shared UI/MCP persistence intact. Resolve upstream text-card dependencies, startup/preview races and runtime upgrades. Co-authored-by: JaysonAlbert --- .github/SECURITY.md | 2 +- CHANGELOG.md | 7 + README.ja.md | 2 + README.md | 4 +- README.zh-CN.md | 4 +- docs/ARCHITECTURE.md | 17 + docs/MCP.md | 11 +- docs/PRODUCT_PLAN_AND_STATUS.md | 18 +- docs/PROJECT_ORGANIZATION.md | 87 + docs/RELEASE_SCOPE.md | 9 + drizzle/sqlite/0004_majestic_thing.sql | 25 + drizzle/sqlite/meta/0004_snapshot.json | 1626 +++++++++++++++++ drizzle/sqlite/meta/_journal.json | 7 + .../skills/beatdesign-workspace/SKILL.md | 19 + integrations/codex/beatdesign/README.md | 2 +- .../skills/beatdesign-workspace/SKILL.md | 21 +- .../skills/beatdesign-workspace/SKILL.md | 11 + integrations/workbuddy/runtime/launcher.mjs | 10 +- integrations/workbuddy/runtime/migrations.mjs | 23 + messages/en.json | 151 ++ messages/ja.json | 151 ++ messages/zh.json | 151 ++ package.json | 2 +- playwright.config.ts | 10 +- scripts/build-workbuddy-runtime-package.ts | 1 + scripts/seed-organization-fixture.ts | 408 +++++ scripts/start-e2e.mjs | 1 + scripts/start-local.mjs | 6 + .../app/project-assets-workspace.tsx | 2 + .../app/project-organization-panel.tsx | 863 +++++++++ .../app/project-organization-workspace.tsx | 185 ++ .../beatcanvas-composer-reference-picker.tsx | 6 +- .../beatcanvas-composer-shell.test.tsx | 38 +- .../beatcanvas/beatcanvas-composer-shell.tsx | 75 +- .../beatcanvas-front-layer-context.tsx | 10 + .../beatcanvas/beatcanvas-front-layer.tsx | 92 +- .../beatcanvas/beatcanvas-shell.tsx | 141 +- .../canvas-organization-presentation.test.ts | 33 + .../beatcanvas/nodes/asset-card-node.tsx | 81 +- .../beatcanvas/nodes/text-card-node.tsx | 107 ++ .../beatcanvas/organization-panel.tsx | 1080 +++++++++++ .../react-flow/beatcanvas-react-flow-types.ts | 46 +- .../react-flow/react-flow-editor.tsx | 809 +++++++- .../react-flow/react-flow-group-usage.test.ts | 50 + .../react-flow-snapshot-parity.test.ts | 6 +- .../use-beatcanvas-generation-runtime.ts | 24 +- .../use-beatcanvas-react-flow-adapter.test.ts | 88 +- .../use-beatcanvas-react-flow-adapter.ts | 288 ++- .../use-project-snapshot-lifecycle.test.ts | 2 +- .../use-project-snapshot-lifecycle.ts | 102 +- .../beatcanvas/workflow-template-panel.tsx | 392 ++++ src/config/db/schema.sqlite.ts | 48 +- src/core/beatcanvas/canvas-types.ts | 29 +- .../generation-controller-persistence.test.ts | 74 + src/core/beatcanvas/generation-controller.ts | 77 +- .../beatcanvas/reference-mentions.test.ts | 2 +- src/core/beatcanvas/reference-mentions.ts | 39 +- src/core/beatcanvas/text-nodes.test.ts | 260 +++ src/core/beatcanvas/text-nodes.ts | 90 + src/core/commands/asset-boundary.ts | 9 +- src/core/commands/canvas-card-repair.ts | 3 + src/core/commands/canvas-commands.ts | 164 +- src/core/commands/canvas-diff.ts | 23 +- .../canvas-organization-integration.test.ts | 109 ++ src/core/commands/conflict-retry.ts | 5 +- src/core/commands/executor.ts | 42 +- src/core/commands/generation-contract.ts | 49 +- src/core/commands/persist.ts | 214 ++- src/core/commands/receipts.ts | 13 +- src/core/commands/schema.ts | 190 +- src/core/db/runtime-migrations.test.ts | 31 + src/core/db/sqlite.ts | 40 +- src/core/db/transaction-context.ts | 23 + src/core/editor/timeline-usage.test.ts | 99 + src/core/editor/timeline-usage.ts | 34 + .../effects/generation-upload-intent.test.ts | 2 + src/core/effects/generation-upload-intent.ts | 39 +- src/core/generation-providers/submit.ts | 142 +- .../organization/canvas-organization.test.ts | 338 ++++ src/core/organization/canvas-organization.ts | 700 +++++++ .../organization/canvas-projection.test.ts | 170 ++ src/core/organization/canvas-projection.ts | 161 ++ src/core/organization/command-contract.ts | 80 + src/core/organization/cross-process.test.ts | 300 +++ ...neration-source-intent.integration.test.ts | 134 ++ .../organization/generation-source.test.ts | 291 +++ src/core/organization/generation-source.ts | 411 +++++ src/core/organization/group-layout.test.ts | 296 +++ src/core/organization/group-layout.ts | 287 +++ .../organization-document.test.ts | 494 +++++ .../organization/organization-document.ts | 500 +++++ src/core/organization/persistence.test.ts | 277 +++ src/core/organization/service.ts | 405 ++++ src/core/projects/project-canvas-document.ts | 36 +- src/core/projects/project-revision.ts | 40 +- src/core/projects/project-snapshot.ts | 73 +- src/core/projects/projects.ts | 39 +- src/core/workflows/service.test.ts | 390 ++++ src/core/workflows/service.ts | 363 ++++ .../workflows/template-preview-contract.ts | 33 + src/core/workflows/workflow-template.test.ts | 342 ++++ src/core/workflows/workflow-template.ts | 809 ++++++++ src/core/workspace-lib/db-adapter.ts | 3 +- src/lib/trusted-local-request.test.ts | 6 +- src/lib/trusted-local-request.ts | 6 +- src/lib/trusted-workspace-host.test.ts | 13 + src/lib/trusted-workspace-host.ts | 15 + src/mcp/generation-canvas.ts | 57 +- src/mcp/organization-tools.ts | 203 ++ src/mcp/server.contract.test.ts | 14 +- src/mcp/server.runtime.test.ts | 333 +++- src/mcp/server.ts | 56 +- src/mcp/stdio-probe.test.ts | 33 +- src/mcp/tools.ts | 8 + .../app/projects/$projectId/assets/index.ts | 86 +- .../$projectId/organization-assets.ts | 62 + .../$projectId/organization-preview.ts | 67 + .../app/projects/$projectId/organization.ts | 41 + .../api/app/projects/$projectId/snapshot.ts | 6 +- .../projects/$projectId/template-preview.ts | 81 + .../app/projects/$projectId/timeline-usage.ts | 25 + .../api/app/workflow-templates/$templateId.ts | 25 + .../api/app/workflow-templates/index.ts | 30 + src/server.ts | 2 + tests/e2e/local-host-security.spec.ts | 13 + tests/e2e/organization.spec.ts | 991 ++++++++++ 126 files changed, 17727 insertions(+), 574 deletions(-) create mode 100644 docs/PROJECT_ORGANIZATION.md create mode 100644 drizzle/sqlite/0004_majestic_thing.sql create mode 100644 drizzle/sqlite/meta/0004_snapshot.json create mode 100644 integrations/workbuddy/runtime/migrations.mjs create mode 100644 scripts/seed-organization-fixture.ts create mode 100644 scripts/start-local.mjs create mode 100644 src/components/app/project-organization-panel.tsx create mode 100644 src/components/app/project-organization-workspace.tsx create mode 100644 src/components/beatcanvas/canvas-organization-presentation.test.ts create mode 100644 src/components/beatcanvas/nodes/text-card-node.tsx create mode 100644 src/components/beatcanvas/organization-panel.tsx create mode 100644 src/components/beatcanvas/react-flow/react-flow-group-usage.test.ts create mode 100644 src/components/beatcanvas/workflow-template-panel.tsx create mode 100644 src/core/beatcanvas/generation-controller-persistence.test.ts create mode 100644 src/core/beatcanvas/text-nodes.test.ts create mode 100644 src/core/beatcanvas/text-nodes.ts create mode 100644 src/core/commands/canvas-organization-integration.test.ts create mode 100644 src/core/db/runtime-migrations.test.ts create mode 100644 src/core/db/transaction-context.ts create mode 100644 src/core/editor/timeline-usage.test.ts create mode 100644 src/core/editor/timeline-usage.ts create mode 100644 src/core/organization/canvas-organization.test.ts create mode 100644 src/core/organization/canvas-organization.ts create mode 100644 src/core/organization/canvas-projection.test.ts create mode 100644 src/core/organization/canvas-projection.ts create mode 100644 src/core/organization/command-contract.ts create mode 100644 src/core/organization/cross-process.test.ts create mode 100644 src/core/organization/generation-source-intent.integration.test.ts create mode 100644 src/core/organization/generation-source.test.ts create mode 100644 src/core/organization/generation-source.ts create mode 100644 src/core/organization/group-layout.test.ts create mode 100644 src/core/organization/group-layout.ts create mode 100644 src/core/organization/organization-document.test.ts create mode 100644 src/core/organization/organization-document.ts create mode 100644 src/core/organization/persistence.test.ts create mode 100644 src/core/organization/service.ts create mode 100644 src/core/workflows/service.test.ts create mode 100644 src/core/workflows/service.ts create mode 100644 src/core/workflows/template-preview-contract.ts create mode 100644 src/core/workflows/workflow-template.test.ts create mode 100644 src/core/workflows/workflow-template.ts create mode 100644 src/lib/trusted-workspace-host.test.ts create mode 100644 src/lib/trusted-workspace-host.ts create mode 100644 src/mcp/organization-tools.ts create mode 100644 src/routes/api/app/projects/$projectId/organization-assets.ts create mode 100644 src/routes/api/app/projects/$projectId/organization-preview.ts create mode 100644 src/routes/api/app/projects/$projectId/organization.ts create mode 100644 src/routes/api/app/projects/$projectId/template-preview.ts create mode 100644 src/routes/api/app/projects/$projectId/timeline-usage.ts create mode 100644 src/routes/api/app/workflow-templates/$templateId.ts create mode 100644 src/routes/api/app/workflow-templates/index.ts create mode 100644 tests/e2e/local-host-security.spec.ts create mode 100644 tests/e2e/organization.spec.ts diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 30a118d..ee717a0 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -8,6 +8,6 @@ Include the affected route or component, reproduction steps, impact, and any sug ## Deployment responsibility -BeatDesign is a single-user, local-first application and does not include authentication. The development server binds to localhost and is not designed to be exposed as a hosted multi-user service. Provider and optional storage credentials are encrypted in the local SQLite workspace. +BeatDesign is a single-user, local-first application and does not include authentication. Supported development and production launchers bind to localhost; production HTTP rejects non-local Host authorities. The workspace is not designed to be exposed as a hosted multi-user service. Provider and optional storage credentials are encrypted in the local SQLite workspace. The maintainers support the latest released `0.2.x` version. Security fixes are published in the next patch release when possible. diff --git a/CHANGELOG.md b/CHANGELOG.md index 441bcc0..1d53251 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to BeatDesign are documented in this file. +## [Unreleased] + +- Adapt Issue #29 organization work from JaysonAlbert/BeatDesign PR #16: persistent Canvas groups and arrangement, logical Asset folders, exact-version Elements, text steps, and reusable local workflow templates. UI and MCP share atomic revision-checked commands; eight added tools bring source MCP to 38. +- Preserve older Canvas documents and pinned Timeline Assets. Template capture stores semantic configuration, not source media identities; insertion never uploads or runs generation. +- Reject non-local production HTTP Host authorities independently of Origin, and pin source production/WorkBuddy listeners to loopback. +- Apply packaged schema upgrades and migration receipts atomically while tolerating additive columns/tables already installed by older source/runtime startup. + ## [0.2.5] - 2026-10-22 ### Added diff --git a/README.ja.md b/README.ja.md index 6651fd7..216a943 100644 --- a/README.ja.md +++ b/README.ja.md @@ -46,6 +46,8 @@ BeatDesignは、AI画像・動画制作のための独立したオープンソ [変更履歴](./CHANGELOG.md) · [Project設定](./docs/PROJECT_SETTINGS.md) · [現状とロードマップ](./docs/PRODUCT_PLAN_AND_STATUS.md) +現在のソース(未リリース)は永続 Canvas グループ、素材フォルダー、固定バージョンの Elements、ローカルワークフローテンプレートと 38 の MCP ツールを含みます。v0.2.5 npm パッケージは 30 ツールです。[詳細](./docs/PROJECT_ORGANIZATION.md)。 + ## 2つの制作モード、1つのProject ### 無限Canvas diff --git a/README.md b/README.md index a537a9b..cf8fdc0 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ Already available: tail-frame continuation, Canvas-to-Editor handoff, image over [Change details](./CHANGELOG.md) · [Project settings](./docs/PROJECT_SETTINGS.md) · [Current scope and roadmap](./docs/PRODUCT_PLAN_AND_STATUS.md) +Current source (unreleased) adds persistent Canvas groups and arrangement, Asset folders, exact-version Elements and reusable local workflow templates, with 38 MCP tools. These changes are not in the v0.2.5 npm package. [Organization guide](./docs/PROJECT_ORGANIZATION.md). + ## Two creative modes, one Project ### Infinite Canvas @@ -104,7 +106,7 @@ Add your own [BeatAPI API key](https://beatapi.io/dashboard/apikeys) only when y ## Use BeatDesign with an Agent -BeatDesign exposes 30 local MCP tools for Skills, Projects, Assets, Canvas, generation, and Editor operations, including built-in Skill discovery and authoritative MP4 timeline rendering. Agent changes use the same project services and become visible in the browser workspace. +Current source exposes 38 local MCP tools (v0.2.5 packages expose 30) for Skills, Projects, Assets, Canvas, generation, and Editor operations, including built-in Skill discovery and authoritative MP4 timeline rendering. Agent changes use the same project services and become visible in the browser workspace. After connecting your MCP host, you can ask: diff --git a/README.zh-CN.md b/README.zh-CN.md index 9d0f348..b855e59 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -46,6 +46,8 @@ BeatDesign 是一个独立、开源、本地优先的 AI 图片与视频创作 [更新详情](./CHANGELOG.md) · [项目设置说明](./docs/PROJECT_SETTINGS.md) · [当前能力与后续规划](./docs/PRODUCT_PLAN_AND_STATUS.md) +当前源码(尚未正式发版)新增持久画布分组与排列、素材文件夹、固定版本 Elements 和本机工作流模板,MCP 共 38 个工具;v0.2.5 npm 包仍为 30 个。[功能说明](./docs/PROJECT_ORGANIZATION.md)。 + ## 两种核心创作模式,一个 Project ### 无限 Canvas @@ -104,7 +106,7 @@ pnpm dev ## 让 Agent 操作 BeatDesign -BeatDesign 提供 30 个本地 MCP 工具,覆盖 Skill、Project、Asset、Canvas、生成和 Editor 操作,包括发现内置 Skill 目录以及从权威时间线导出 MP4。Agent 修改会经过同一套项目服务,并显示在浏览器工作空间中。 +当前源码提供 38 个本地 MCP 工具(v0.2.5 发布包为 30 个),覆盖 Skill、Project、Asset、Canvas、生成和 Editor 操作,包括发现内置 Skill 目录以及从权威时间线导出 MP4。Agent 修改会经过同一套项目服务,并显示在浏览器工作空间中。 连接 MCP Host 后,可以直接提出这样的要求。 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a6ef648..b99439b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -36,8 +36,25 @@ Provider and optional R2/S3 credentials are encrypted in the local `config` tabl Upload storage is a separate adapter boundary. File selection remains browser-local. A successful generation precheck creates a short-lived, one-time SQLite intent that binds the project, model, exact upload count, uploaded URLs, and final generation submission. Required references are promoted only after that point and immediately before task submission; they become project assets only after the provider accepts the task. The default path uploads supported references to BeatAPI Files. Users may instead configure a public R2/S3-compatible bucket; those credentials remain local and are used only for confirmed generation inputs. +Production browser requests accept only localhost, IPv4 loopback and IPv6 loopback +authorities, independently of Origin. `pnpm start` and the WorkBuddy launcher pin +the listener to IPv4 loopback; ambient hosting variables cannot widen it. Packaged +DDL and migration receipts commit atomically and tolerate prior additive startup +upgrades without rewriting user records. + ## Command boundary +Project organization uses this same boundary. Canvas v4 stores canonical groups, +fixed reference bindings and frozen template instances alongside unchanged cards +and absolute frames. Display projections never replace raw references or historical +input snapshots. Per-project folders and Elements live in +`project_organization_state`; installation-wide definitions live in `workflow_template`. +New semantic writes require explicit revisions and stable IDs. An asynchronous +transaction context keeps service reads, CAS updates and request-fingerprinted +receipts on the same SQLite transaction, including legacy snapshot save paths. +Canvas generation intents additionally pin the reviewed source card/configuration +digest. See [Project organization](PROJECT_ORGANIZATION.md). + UI actions and the local MCP server call the same Command Kernel. The kernel owns validation and pure document operations for Canvas and Editor. Persistence boundaries remain responsible for revision checks, durable writes, and returning the authoritative document. ```text diff --git a/docs/MCP.md b/docs/MCP.md index 7658fbf..7db2052 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -104,7 +104,7 @@ installation shapes. ## Tool groups -There are **30** tools: +The current source exposes **38** tools (the published v0.2.5 package has 30): - Skill (2): list the bundled official Skill catalog and read one complete versioned Skill definition. @@ -121,6 +121,15 @@ There are **30** tools: - Editor (8): get, incremental edit, SRT import, authoritative MP4 render, semantic snapshot, diagnostics, deep-link view, and command history. +Organization adds eight tools: `bdesign_organization_get`, +`bdesign_organization_apply`, `bdesign_asset_query`, +`bdesign_workflow_template_list`, `bdesign_workflow_template_get`, +`bdesign_workflow_template_preview`, `bdesign_workflow_template_apply`, and +`bdesign_timeline_usage`. New writes require explicit command IDs, idempotency +keys and the revision for the command domain. Preview before capturing templates +or changing exact Element bindings; instantiation never submits generation. +See [Project organization](PROJECT_ORGANIZATION.md). + The same bundled Skill catalog is also available through the read-only MCP Resource `beatdesign://skills`. Individual Skill instructions use the resource template `beatdesign://skills/{skillId}`. The two Skill tools are compatibility diff --git a/docs/PRODUCT_PLAN_AND_STATUS.md b/docs/PRODUCT_PLAN_AND_STATUS.md index 9b096ce..951ce69 100644 --- a/docs/PRODUCT_PLAN_AND_STATUS.md +++ b/docs/PRODUCT_PLAN_AND_STATUS.md @@ -12,7 +12,7 @@ - `bdesign_project_update` 将 MCP 目录扩展为 30 个工具;项目设置更新与幂等回执在同一 SQLite 事务提交,回执失败时回滚设置,保留旧项目封面。 - 浏览器/MCP 检查使用生产构建;画布完成编辑器与项目初始化后才开放工具栏和快捷键,延迟加载回归检查确认首次创建动作可保存。 - 从 PR #18 提取 Windows 测试兼容、Studio 首屏翻译、空画布撤销/重做保存及窄屏布局修复;第三方品牌、Provider 和托管部署配置未纳入上游。 -- v0.2.5 补齐跨平台安装脚本、Linux/Windows CI、接入包版本和项目设置 Skill 指引;README 列出 v0.2.5 功能、正式发布入口与源码升级步骤。官方 Skill 目录仍为空,Issue #29 的持久分组、素材文件夹和复用模板仍待实现。 +- v0.2.5 补齐跨平台安装脚本、Linux/Windows CI、接入包版本和项目设置 Skill 指引;README 列出 v0.2.5 功能、正式发布入口与源码升级步骤。官方 Skill 目录仍为空;后续源码改进另见下方,未包含在 v0.2.5 发布包。 ### v0.2.4 已发布改进 @@ -23,6 +23,18 @@ - Studio / Canvas / Editor redo 与 MCP 共用 asset-first 服务端预检及提交;浏览器接口使用逻辑 model ID,Provider 状态同步集中到服务端。 - 本地 SQLite 启用 WAL 和 busy timeout;MCP 版本读取根 package 元数据。浏览器与 MCP 联动测试使用独立临时数据库,不调用付费 Provider。 +### 后续源码改进(未正式发版) + +从 JaysonAlbert 的 Fork PR #16 中提取 Issue #29 的持久命名分组、折叠和自动排列、 +逻辑素材文件夹、固定版本 Elements、可复用本机工作流模板与文字步骤。 +保留现有项目及 BeatAPI Provider;未引入 Fork 的其他 Provider 或 Series/Scenes 架构。 +UI/MCP 共用事务命令、revision、幂等回执;整理和模板插入不创建生成任务、不上传。 +新增 8 个 MCP 工具,当前源码共 38 个;v0.2.5 发布包仍为 30 个。 + +补充生产 HTTP Host 校验、`pnpm start`/WorkBuddy 回环绑定,以及兼容旧数据库的 +原子安装迁移。功能边界见 [PROJECT_ORGANIZATION.md](PROJECT_ORGANIZATION.md)。 +源码、自动检查、浏览器/MCP 验证、合并和正式发版分别记录;这里不代表 npm 已更新。 + ## 1. BeatDesign 是什么 BeatDesign 是一个免费、开源、本地优先的 AI 图片/视频创作工作台。它把 Studio、节点 Canvas、项目 Assets、本地视频 Editor、生成历史和 BeatAPI 连接放在同一个 Project 中。 @@ -173,7 +185,7 @@ Codex / Claude Code / Other Agent - `AssetFirstGenerationRequest` v2 不保存通用首帧或尾帧角色;大多数模型仍由明确的 `@ImageN` Prompt 指令表达首尾帧语义。MiniMax H3 Max / Max Turbo 按其公开契约使用专用双槽位,并将附件顺序映射为首帧、尾帧。Adapter 负责翻译 Provider 字段和校验模型硬限制。 - UI 命令入口不再接受客户端 `origin`;服务端固定写入 `ui`,MCP 入口在内核边界固定写入 `mcp`。 - Provider Contract 已将逻辑模型目录与 BeatAPI effectId、上传路径和上游模型名拆开;当前同步 GPT Image 2.5 Flare/Sunburst、Wan 3.0/Prime、HappyHorse 1.0/1.1、MiniMax H3 Max/Max Turbo 的请求契约和 SVG 品牌标志。BeatAPI 是默认实现,Fork 可在源码扩展点注册其他 Provider。 -- 本地 stdio MCP Server 提供 30 个工具(Skill / Project / Asset / Canvas / Generation / Editor),模型和参数通过 capability discovery 暴露;Canvas / Editor 增量操作使用完整 JSON Schema,Agent 可直接发现操作类型和参数。MCP 生成直接调用当前 Provider,本地产品不重复实现 API Key、余额、计费或限流策略,只透传 Provider 的结果与错误。`bdesign_skill_list` / `bdesign_skill_get` 是不显示 MCP Resources 的宿主兼容入口;`bdesign_project_target` 绑定当前会话项目,Project/Canvas/Editor view 工具返回 Codex Browser handoff;`bdesign_asset_import` 把本地文件导入项目 Asset 库;`bdesign_asset_extract_frame` 与 `bdesign_canvas_continue_from_tail` 负责抽帧续写;Editor MCP 可导入 SRT、放置和替换任意项目图片 Overlay、调整叠层与单条字幕参数,并通过 `bdesign_editor_render` 将权威时间线导出为项目内 MP4 Asset。 +- 本地 stdio MCP Server 当前源码提供 38 个工具(Skill / Project / Asset / Canvas / Generation / Editor),模型和参数通过 capability discovery 暴露;Canvas / Editor 增量操作使用完整 JSON Schema,Agent 可直接发现操作类型和参数。MCP 生成直接调用当前 Provider,本地产品不重复实现 API Key、余额、计费或限流策略,只透传 Provider 的结果与错误。`bdesign_skill_list` / `bdesign_skill_get` 是不显示 MCP Resources 的宿主兼容入口;`bdesign_project_target` 绑定当前会话项目,Project/Canvas/Editor view 工具返回 Codex Browser handoff;`bdesign_asset_import` 把本地文件导入项目 Asset 库;`bdesign_asset_extract_frame` 与 `bdesign_canvas_continue_from_tail` 负责抽帧续写;Editor MCP 可导入 SRT、放置和替换任意项目图片 Overlay、调整叠层与单条字幕参数,并通过 `bdesign_editor_render` 将权威时间线导出为项目内 MP4 Asset。 - Codex、Claude Code 与 WorkBuddy 接入包内含 `beatdesign-workspace` Skill,负责项目选择、字幕/续写工具编排、付费生成停点和可视化复核;三者共用同一 MCP 与本地 Project 数据,其中 Claude Code 和 WorkBuddy 使用本机 HTTP MCP。 ## 6. v0.2 Phase 1 本地已实现 @@ -188,7 +200,7 @@ Codex / Claude Code / Other Agent - Canvas -> Timeline Node -> Editor 连续工作流。 - Editor 自动保存接入命令入口,并补齐冲突三方合并、重复操作保护和稳定播放头时间。 - 图片 Clip、时间线拖拽调整持续时间与图片/视频统一视觉轨。 -- 本地 MCP Server 提供 30 个 Skill、Project、Asset、Canvas、Generation、Editor 工具;支持内置 Skill 目录发现、会话项目绑定、Canvas/Editor 可视化交接、从绝对路径导入本地素材、抽取尾帧续写、导入和精调 SRT 字幕、放置/替换/调整图片 Overlay,以及权威时间线 MP4 导出。 +- 本地 MCP Server 当前源码提供 38 个 Skill、Project、Asset、Canvas、Generation、Editor 工具;支持内置 Skill 目录发现、会话项目绑定、Canvas/Editor 可视化交接、从绝对路径导入本地素材、抽取尾帧续写、导入和精调 SRT 字幕、放置/替换/调整图片 Overlay,以及权威时间线 MP4 导出。 - 已加入只读 `beatdesign://skills` MCP Resource、单 Skill Resource Template、schema v1 清单校验和宿主兼容工具;当前官方创作 Skill 目录有意保持为空,待具体 Skill 完成提示词评测和可见工作流联调后再加入,并在那时开放 Showcase 入口。 - MCP 增量 Canvas/Editor 命令在短暂 revision 竞争时会基于最新权威文档限次自动重放;持续冲突返回最新 revision 和明确重试提示。 - Canvas 与 Editor 每 2 秒并在页面重新聚焦时检查 MCP 写入的新 revision。 diff --git a/docs/PROJECT_ORGANIZATION.md b/docs/PROJECT_ORGANIZATION.md new file mode 100644 index 0000000..d4bf440 --- /dev/null +++ b/docs/PROJECT_ORGANIZATION.md @@ -0,0 +1,87 @@ +# Project organization + +This feature uses the existing local Project, Assets and Command Kernel. It adds +organization metadata; it does not migrate production projects or run generation. +The implementation is adapted from JaysonAlbert/BeatDesign draft PR #16, +organization commits `e4ae849` and `2176e20`, with upstream compatibility fixes. +General upstream tracking: [Issue 29](https://github.com/BeatAPI/BeatDesign/issues/29). + +## User controls + +- Canvas → **Organize Canvas**: preview a group from the selected nodes, name it, + choose its purpose, expand/collapse, change members, choose a displayed result + or ungroup. Dragging moves the group and its canonical members together; + resizing changes the frame. Arrange contents or multiple groups in rows, + columns or a grid; collapsed groups retain their canonical member positions. Ungrouping keeps cards and source media. +- **Show all versions** opens the grouped take inspector, including failed + attempts. Preview is read-only: it does not pin a result or change Timeline. +- Assets → **Project organization**: logical folders, explicit project/current-folder + search and paged results. Narrow screens use an accessible drawer with Escape, + focus return and retained form input. Removing a folder previews promotion of its assets and + children to the parent. Files and membership categories do not move. +- **Elements**: create a named character, scene, prop or voice from an existing + Asset, append versions and choose the current version. Canvas inputs pin an exact + version; changing current only asks consumers to review. Keep/adopt is explicit. +- **Workflow templates**: preview selected text/generation steps and ordered input + slots, then save locally. Insertions are independent editable drafts. Bind slots + to target-project Assets or Element versions; missing required slots block generation. + Library rename/removal and source-project deletion do not rewrite saved instances. +- **View Timeline usage** reads the actual source helper, including active takes, + source intervals, hidden/muted tracks and captions. Displayed results remain distinct. + +## Storage and command boundary + +Canvas v4 adds groups, exact reference bindings, frozen template-instance slots and +view preferences. Readers accept older documents without writing them on read. +Hidden output cards, absolute frames, generation snapshots and raw reference IDs +remain authoritative. Proxies and aggregated edges are display-only. + +`project_organization_state` stores per-project logical folders and Elements. +`workflow_template` stores this installation's reusable definitions. Startup upgrades +are additive and repeatable. Existing source media is never copied into templates. + +UI and MCP use `persistBeatDesignCommand`. New commands and expanded Canvas +operations require explicit command ID, idempotency key and expected revision. +State changes and receipts share an immediate SQLite transaction. Same-key changed +content rejects; semantic conflicts do not automatically adopt a later revision. +All v4 saves revalidate final project/Element bindings, including undo and old operations. + +Canvas generation carries a source card. Preflight and submission verify required +slots, exact references and a digest of reviewed configuration. Intents preserve that +digest; changed inputs require another review. This does not change provider routes. + +## Interfaces + +UI writes use `/api/app/projects/:id/commands`. Reads include `organization`, +`organization-preview`, `assets` (GET; `organization-assets` is an equivalent alias), +`timeline-usage`, `template-preview` and `/api/app/workflow-templates/` with detail by ID. +Revision queries include the organization revision; an absent organization is revision zero. + +MCP adds organization get/apply, asset query, workflow-template list/get/preview/apply +and Timeline usage. Existing asset-list results stay arrays and deduplicate membership. +See `src/core/organization/command-contract.ts` and `canvas-organization.ts` +for command fields. Folder/Element commands use organization revision, Canvas +commands use snapshot revision, template rename/removal use template revision; +new-template capture requires zero and a matching source-Canvas preview. + +Limits remain 200 Canvas records, 20 inputs per card and 2 MiB per Canvas document. +Additional limits are 64 groups, 100 instances, 500 folders/eight parent levels, +256 Elements/100 versions each, and 100 templates/128 KiB per definition. + +## Verification and isolated preview + +`pnpm build && pnpm test:e2e` runs the production UI/MCP checks. The fixture +script `scripts/seed-organization-fixture.ts` requires +an explicitly isolated `BEATDESIGN_DATA_DIR`; it refuses the default production data +directory. It creates 140 synthetic Canvas records, eight groups/twelve overview +entries, over 500 Assets, versioned Elements and an actual Timeline with older takes. + +Focused tests cover invariant rejection, receipt-failure rollback, cross-process CAS, +Element binding/removal races, private-template boundaries, replay after receipt expiry, +stale generation intents and active-take ranges. Organization paths assert zero +provider jobs, upload intents and paid calls. Verification results are recorded in `RELEASE_SCOPE.md`; source changes are +not part of an existing published package. + +Source checks, isolated preview, human visual acceptance, merge and deployment are +separate states. This branch does not authorize real-film organization or main-instance +replacement. diff --git a/docs/RELEASE_SCOPE.md b/docs/RELEASE_SCOPE.md index df97a9e..7c34b25 100644 --- a/docs/RELEASE_SCOPE.md +++ b/docs/RELEASE_SCOPE.md @@ -8,6 +8,15 @@ Excluded: authentication, login, accounts, payments, subscriptions, credits, API Release verification requires a clean install, schema creation, production build (which generates Paraglide and route types), typecheck, test, i18n check, MCP stdio handshake, and local route smoke test. Browser/MCP checks run against that production build; run `pnpm build` before `pnpm test:e2e`. A real paid BeatAPI generation is a separate credentialed end-to-end check. +## Current source — project organization (unreleased, 2026-10-22) + +- Adapt Issue #29 organization commits from JaysonAlbert/BeatDesign draft PR #16; preserve official provider and shared local workspace boundaries. +- Local checks passed: typecheck, 504 tests (no skips), i18n validation and production build. All 25 production browser/MCP checks pass, including English/Chinese organization, mobile focus handling, exact pinned inputs, template insertion visibility and local Host rejection. +- A separate pure compatibility check accepted Canvas default controls for all 26 official model descriptors without provider requests. +- Build the runtime tarball, install it in a clean temporary directory, complete the 38-tool stdio handshake and route smoke checks. Additive migration regression covers previously upgraded columns, repeated schema setup, retained project data and rollback after a failed migration. +- WorkBuddy Connector structure check passes; production dependency audit reports no known vulnerabilities at verification time. +- Current source adds eight MCP tools; the published v0.2.5 package remains at 30. No new version, GitHub Release, npm publication or WorkBuddy submission is claimed by these checks. Credentialed/paid generation remains a separate unperformed check. + ## v0.2.5 release (2026-10-22) - [x] Align application, Codex, Claude Code, Claude marketplace, LobeHub and WorkBuddy metadata on `0.2.5`. diff --git a/drizzle/sqlite/0004_majestic_thing.sql b/drizzle/sqlite/0004_majestic_thing.sql new file mode 100644 index 0000000..234007f --- /dev/null +++ b/drizzle/sqlite/0004_majestic_thing.sql @@ -0,0 +1,25 @@ +CREATE TABLE `project_organization_state` ( + `project_id` text PRIMARY KEY NOT NULL, + `schema_version` integer DEFAULT 1 NOT NULL, + `document_json` text NOT NULL, + `version` integer DEFAULT 1 NOT NULL, + `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `workflow_template` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `schema_version` integer DEFAULT 1 NOT NULL, + `definition_json` text NOT NULL, + `revision` integer DEFAULT 1 NOT NULL, + `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL +); +--> statement-breakpoint +ALTER TABLE `generation_upload_intent` ADD `source_card_id` text;--> statement-breakpoint +ALTER TABLE `generation_upload_intent` ADD `source_context_digest` text;--> statement-breakpoint +ALTER TABLE `project` ADD `description` text DEFAULT '' NOT NULL;--> statement-breakpoint +ALTER TABLE `project` ADD `cover_mode` text DEFAULT 'manual' NOT NULL;--> statement-breakpoint +ALTER TABLE `project` ADD `settings_revision` integer DEFAULT 1 NOT NULL;--> statement-breakpoint +ALTER TABLE `project_command_receipt` ADD `request_hash` text; \ No newline at end of file diff --git a/drizzle/sqlite/meta/0004_snapshot.json b/drizzle/sqlite/meta/0004_snapshot.json new file mode 100644 index 0000000..eaf9573 --- /dev/null +++ b/drizzle/sqlite/meta/0004_snapshot.json @@ -0,0 +1,1626 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "bf06caaa-7d9c-44a9-9cbd-463b96f69b1b", + "prevId": "6622e3e9-3968-4f55-a0a6-cf8538d1a594", + "tables": { + "config": { + "name": "config", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "generation_asset_link": { + "name": "generation_asset_link", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "generation_asset_link_generation_idx": { + "name": "generation_asset_link_generation_idx", + "columns": [ + "generation_id" + ], + "isUnique": false + }, + "generation_asset_link_asset_idx": { + "name": "generation_asset_link_asset_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + }, + "generation_asset_link_unique": { + "name": "generation_asset_link_unique", + "columns": [ + "generation_id", + "asset_id", + "role" + ], + "isUnique": true + } + }, + "foreignKeys": { + "generation_asset_link_generation_id_generation_history_id_fk": { + "name": "generation_asset_link_generation_id_generation_history_id_fk", + "tableFrom": "generation_asset_link", + "tableTo": "generation_history", + "columnsFrom": [ + "generation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generation_asset_link_asset_id_asset_id_fk": { + "name": "generation_asset_link_asset_id_asset_id_fk", + "tableFrom": "generation_asset_link", + "tableTo": "asset", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "generation_history": { + "name": "generation_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_task_id": { + "name": "provider_task_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lifecycle_phase": { + "name": "lifecycle_phase", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_provider_sync_at": { + "name": "last_provider_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "execution_mode": { + "name": "execution_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'create_new'" + }, + "submitted_prompt": { + "name": "submitted_prompt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "submitted_params": { + "name": "submitted_params", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_asset_id": { + "name": "result_asset_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input": { + "name": "input", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "output": { + "name": "output", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "failed_at": { + "name": "failed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "generation_history_project_idx": { + "name": "generation_history_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "generation_history_effect_idx": { + "name": "generation_history_effect_idx", + "columns": [ + "effect_id" + ], + "isUnique": false + }, + "generation_history_status_idx": { + "name": "generation_history_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "generation_history_provider_task_idx": { + "name": "generation_history_provider_task_idx", + "columns": [ + "provider_task_id" + ], + "isUnique": false + }, + "generation_history_lifecycle_idx": { + "name": "generation_history_lifecycle_idx", + "columns": [ + "lifecycle_phase" + ], + "isUnique": false + }, + "generation_history_result_asset_idx": { + "name": "generation_history_result_asset_idx", + "columns": [ + "result_asset_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "generation_history_project_id_project_id_fk": { + "name": "generation_history_project_id_project_id_fk", + "tableFrom": "generation_history", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "generation_history_result_asset_id_asset_id_fk": { + "name": "generation_history_result_asset_id_asset_id_fk", + "tableFrom": "generation_history", + "tableTo": "asset", + "columnsFrom": [ + "result_asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "generation_intent_upload": { + "name": "generation_intent_upload", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "intent_id": { + "name": "intent_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'reserved'" + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_url": { + "name": "public_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "completed_at": { + "name": "completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "generation_intent_upload_intent_idx": { + "name": "generation_intent_upload_intent_idx", + "columns": [ + "intent_id" + ], + "isUnique": false + }, + "generation_intent_upload_status_idx": { + "name": "generation_intent_upload_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": { + "generation_intent_upload_intent_id_generation_upload_intent_id_fk": { + "name": "generation_intent_upload_intent_id_generation_upload_intent_id_fk", + "tableFrom": "generation_intent_upload", + "tableTo": "generation_upload_intent", + "columnsFrom": [ + "intent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "generation_upload_intent": { + "name": "generation_upload_intent", + "columns": { + "source_card_id": { + "name": "source_card_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "source_context_digest": { + "name": "source_context_digest", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "effect_id": { + "name": "effect_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expected_upload_count": { + "name": "expected_upload_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reserved_upload_count": { + "name": "reserved_upload_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "completed_upload_count": { + "name": "completed_upload_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "generation_id": { + "name": "generation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "generation_upload_intent_project_idx": { + "name": "generation_upload_intent_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "generation_upload_intent_status_expiry_idx": { + "name": "generation_upload_intent_status_expiry_idx", + "columns": [ + "status", + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "generation_upload_intent_project_id_project_id_fk": { + "name": "generation_upload_intent_project_id_project_id_fk", + "tableFrom": "generation_upload_intent", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "generation_upload_intent_generation_id_generation_history_id_fk": { + "name": "generation_upload_intent_generation_id_generation_history_id_fk", + "tableFrom": "generation_upload_intent", + "tableTo": "generation_history", + "columnsFrom": [ + "generation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project": { + "name": "project", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "cover_mode": { + "name": "cover_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "settings_revision": { + "name": "settings_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "cover_asset_id": { + "name": "cover_asset_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "current_state_version": { + "name": "current_state_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_workspace_mode": { + "name": "last_workspace_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'canvas'" + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived_at": { + "name": "archived_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "project_status_idx": { + "name": "project_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "project_updated_at_idx": { + "name": "project_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + }, + "project_last_opened_at_idx": { + "name": "project_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_asset_membership": { + "name": "project_asset_membership", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_id": { + "name": "asset_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_run_id": { + "name": "source_run_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workflow_type": { + "name": "workflow_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "slot_id": { + "name": "slot_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "project_asset_membership_project_idx": { + "name": "project_asset_membership_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "project_asset_membership_asset_idx": { + "name": "project_asset_membership_asset_idx", + "columns": [ + "asset_id" + ], + "isUnique": false + }, + "project_asset_membership_unique": { + "name": "project_asset_membership_unique", + "columns": [ + "project_id", + "asset_id", + "category" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_asset_membership_project_id_project_id_fk": { + "name": "project_asset_membership_project_id_project_id_fk", + "tableFrom": "project_asset_membership", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_asset_membership_asset_id_asset_id_fk": { + "name": "project_asset_membership_asset_id_asset_id_fk", + "tableFrom": "project_asset_membership", + "tableTo": "asset", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_asset_membership_source_run_id_generation_history_id_fk": { + "name": "project_asset_membership_source_run_id_generation_history_id_fk", + "tableFrom": "project_asset_membership", + "tableTo": "generation_history", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_canvas_state": { + "name": "project_canvas_state", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "document_json": { + "name": "document_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "project_canvas_state_updated_at_idx": { + "name": "project_canvas_state_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_canvas_state_project_id_project_id_fk": { + "name": "project_canvas_state_project_id_project_id_fk", + "tableFrom": "project_canvas_state", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_command_receipt": { + "name": "project_command_receipt", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_id": { + "name": "command_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command_type": { + "name": "command_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_json": { + "name": "result_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "project_command_receipt_project_idx": { + "name": "project_command_receipt_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "project_command_receipt_command_idx": { + "name": "project_command_receipt_command_idx", + "columns": [ + "command_id" + ], + "isUnique": false + }, + "project_command_receipt_idempotency_unique": { + "name": "project_command_receipt_idempotency_unique", + "columns": [ + "project_id", + "idempotency_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_command_receipt_project_id_project_id_fk": { + "name": "project_command_receipt_project_id_project_id_fk", + "tableFrom": "project_command_receipt", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_organization_state": { + "name": "project_organization_state", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "document_json": { + "name": "document_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organization_state_project_id_project_id_fk": { + "name": "project_organization_state_project_id_project_id_fk", + "tableFrom": "project_organization_state", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_timeline_state": { + "name": "project_timeline_state", + "columns": { + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "document_json": { + "name": "document_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "project_timeline_state_updated_at_idx": { + "name": "project_timeline_state_updated_at_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "project_timeline_state_project_id_project_id_fk": { + "name": "project_timeline_state_project_id_project_id_fk", + "tableFrom": "project_timeline_state", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "project_workflow_state": { + "name": "project_workflow_state", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workflow_type": { + "name": "workflow_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workflow_instance_id": { + "name": "workflow_instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "template_slug": { + "name": "template_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "form_json": { + "name": "form_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "layout_json": { + "name": "layout_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selection_json": { + "name": "selection_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "project_workflow_state_project_idx": { + "name": "project_workflow_state_project_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "project_workflow_state_unique": { + "name": "project_workflow_state_unique", + "columns": [ + "project_id", + "workflow_type", + "workflow_instance_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "project_workflow_state_project_id_project_id_fk": { + "name": "project_workflow_state_project_id_project_id_fk", + "tableFrom": "project_workflow_state", + "tableTo": "project", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "asset": { + "name": "asset", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_class": { + "name": "asset_class", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'original'" + }, + "storage_provider": { + "name": "storage_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'beatapi'" + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_url": { + "name": "public_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "origin_project_id": { + "name": "origin_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "thumbnail_asset_id": { + "name": "thumbnail_asset_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "asset_type_idx": { + "name": "asset_type_idx", + "columns": [ + "type" + ], + "isUnique": false + }, + "asset_class_idx": { + "name": "asset_class_idx", + "columns": [ + "asset_class" + ], + "isUnique": false + }, + "asset_origin_project_idx": { + "name": "asset_origin_project_idx", + "columns": [ + "origin_project_id" + ], + "isUnique": false + }, + "asset_created_at_idx": { + "name": "asset_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "asset_bucket_object_key_unique": { + "name": "asset_bucket_object_key_unique", + "columns": [ + "bucket", + "object_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "asset_origin_project_id_project_id_fk": { + "name": "asset_origin_project_id_project_id_fk", + "tableFrom": "asset", + "tableTo": "project", + "columnsFrom": [ + "origin_project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "asset_thumbnail_asset_id_asset_id_fk": { + "name": "asset_thumbnail_asset_id_asset_id_fk", + "tableFrom": "asset", + "tableTo": "asset", + "columnsFrom": [ + "thumbnail_asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workflow_template": { + "name": "workflow_template", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "definition_json": { + "name": "definition_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 4b1fca2..e6acbc1 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1788060451792, "tag": "0003_slow_the_phantom", "breakpoints": true + }, + { + "idx": 4, + "version": "6", + "when": 1792608545998, + "tag": "0004_majestic_thing", + "breakpoints": true } ] } \ No newline at end of file diff --git a/integrations/claude-code/beatdesign/skills/beatdesign-workspace/SKILL.md b/integrations/claude-code/beatdesign/skills/beatdesign-workspace/SKILL.md index c16f0e6..9b06bc8 100644 --- a/integrations/claude-code/beatdesign/skills/beatdesign-workspace/SKILL.md +++ b/integrations/claude-code/beatdesign/skills/beatdesign-workspace/SKILL.md @@ -51,6 +51,25 @@ opened when the host cannot open it. - Read the saved settings back and inspect the project card or workspace header. Project settings refresh while visible within about five seconds. +## Organize assets and reuse workflows + +- Discover the installed tool catalog first; organization tools are available in + current source and are not included in the published v0.2.5 runtime. +- Use `bdesign_organization_get` / `bdesign_asset_query` for folders and versioned + Elements. Folder changes are logical; they do not move media files. +- Apply semantic operations with explicit command ID, idempotency key and the + correct expected revision. Organization, Canvas and template revisions are + separate. On a semantic conflict, read and review again; do not adopt a newer + revision automatically. Reuse the original envelope if delivery is uncertain. +- Element references pin exact Asset/version IDs. Changing the current version + does not silently replace existing references or Timeline clips. +- Use `bdesign_workflow_template_preview` before capture/instantiation, then + `bdesign_workflow_template_apply`. Keep the reviewed digest and source revision. + Insertions create editable drafts only; missing required slots block generation. +- Inspect the result in Canvas/Assets. Use `bdesign_timeline_usage` to check actual + pinned Timeline usage. Organizing, previewing or loading a template must never + trigger generation or upload. + ## Import captions - Prefer `bdesign_editor_import_srt` for a file or complete SRT text. Use diff --git a/integrations/codex/beatdesign/README.md b/integrations/codex/beatdesign/README.md index cdd8f41..ed93194 100644 --- a/integrations/codex/beatdesign/README.md +++ b/integrations/codex/beatdesign/README.md @@ -8,7 +8,7 @@ Canvas or Editor URL to Codex's in-app Browser for visible review. ```text Browser: pnpm dev → http://127.0.0.1:3020 (Canvas/Editor review surface) -Agent: MCP stdio → pnpm mcp (Agent calls 30 tools) +Agent: MCP stdio → pnpm mcp (current source: 38 tools; v0.2.5 package: 30) Both processes share the same local SQLite + project files. ``` diff --git a/integrations/codex/beatdesign/skills/beatdesign-workspace/SKILL.md b/integrations/codex/beatdesign/skills/beatdesign-workspace/SKILL.md index 17e7f90..c95b306 100644 --- a/integrations/codex/beatdesign/skills/beatdesign-workspace/SKILL.md +++ b/integrations/codex/beatdesign/skills/beatdesign-workspace/SKILL.md @@ -20,7 +20,7 @@ For Canvas-specific review, call `bdesign_canvas_view` with `cardId` so the work - Read the current Canvas or Editor state before editing. - Use incremental Canvas and Editor operations with stable IDs and a stable idempotency key. Never replace a complete Canvas or Timeline document. -- External incremental commands automatically retry a short revision race against the newest authoritative document. If a result still includes `retry`, follow its instruction once after reading the latest state; do not busy-loop. +- Legacy incremental commands may retry a short revision race against the newest authoritative document; organization and template commands require explicit conflict review. If a result still includes `retry`, follow its instruction once after reading the latest state; do not busy-loop. - Import local media through `bdesign_asset_import` before referencing it from Canvas or Editor. - Treat generation as asset-first: generated output becomes a Project Asset before placement. @@ -38,6 +38,25 @@ For Canvas-specific review, call `bdesign_canvas_view` with `cardId` so the work - Read the saved settings back and inspect the project card or workspace header. Project settings refresh while visible within about five seconds. +## Organize assets and reuse workflows + +- Discover the installed tool catalog first; organization tools are available in + current source and are not included in the published v0.2.5 runtime. +- Use `bdesign_organization_get` / `bdesign_asset_query` for folders and versioned + Elements. Folder changes are logical; they do not move media files. +- Apply semantic operations with explicit command ID, idempotency key and the + correct expected revision. Organization, Canvas and template revisions are + separate. On a semantic conflict, read and review again; do not adopt a newer + revision automatically. Reuse the original envelope if delivery is uncertain. +- Element references pin exact Asset/version IDs. Changing the current version + does not silently replace existing references or Timeline clips. +- Use `bdesign_workflow_template_preview` before capture/instantiation, then + `bdesign_workflow_template_apply`. Keep the reviewed digest and source revision. + Insertions create editable drafts only; missing required slots block generation. +- Inspect the result in Canvas/Assets. Use `bdesign_timeline_usage` to check actual + pinned Timeline usage. Organizing, previewing or loading a template must never + trigger generation or upload. + ## Import captions - Prefer `bdesign_editor_import_srt` for an SRT file or complete SRT text. Use `bdesign_editor_edit` with `upsert_caption` only for targeted cue changes; do not represent captions as generic media clips. diff --git a/integrations/workbuddy/beatdesign/skills/beatdesign-workspace/SKILL.md b/integrations/workbuddy/beatdesign/skills/beatdesign-workspace/SKILL.md index 22df6d2..4690ca1 100644 --- a/integrations/workbuddy/beatdesign/skills/beatdesign-workspace/SKILL.md +++ b/integrations/workbuddy/beatdesign/skills/beatdesign-workspace/SKILL.md @@ -79,3 +79,14 @@ BeatDesign MCP 是结构化控制层,浏览器中的 BeatDesign 是用户审 写入后核对返回的 revision 和变更 ID,再调用对应 view tool。Canvas 和 Editor 当前通常会在约两秒内或页面重新聚焦时读到 Agent 修改。最终提供准确的 `workspaceUrl` 供用户审核;仅有数据库 revision 不代表用户已经看到结果。 未获明确授权时,不要导出、提交付费生成或额外创建多个版本。 + +## 项目组织与复用模板 + +先发现当前工具目录:组织工具已加入后续源码,v0.2.5 已发布运行包仍不包含。 +使用 `bdesign_organization_get`、`bdesign_asset_query` 查询逻辑目录和 Elements; +组织、Canvas、模板分别使用对应 revision,写入必须有稳定 commandId 和幂等键。 +语义冲突先重新读取和审查,不自动采用最新 revision。固定引用使用具体 Asset 与版本, +当前版本切换不替换已采用的 Timeline 素材。模板先 preview,再 capture/instantiate, +保持预览摘要和源版本。插入只创建可编辑草稿,缺少必需输入时阻止生成; +整理、预览、模板载入均不得自动生成或上传。写入后检查 Canvas/Assets, +通过 `bdesign_timeline_usage` 核对时间线实际采用。 diff --git a/integrations/workbuddy/runtime/launcher.mjs b/integrations/workbuddy/runtime/launcher.mjs index 51ecd89..504aee6 100644 --- a/integrations/workbuddy/runtime/launcher.mjs +++ b/integrations/workbuddy/runtime/launcher.mjs @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { applyRuntimeMigration } from './migrations.mjs'; import { spawn } from 'node:child_process'; import { mkdir, readFile, readdir } from 'node:fs/promises'; import { homedir } from 'node:os'; @@ -57,12 +58,7 @@ const migrateDatabase = async () => { for (const name of migrationFiles) { if (applied.has(name)) continue; const source = await readFile(resolve(migrationsDirectory, name), 'utf8'); - const sql = source.replaceAll('--> statement-breakpoint', ''); - await client.executeMultiple(sql); - await client.execute({ - sql: 'INSERT INTO __beatdesign_runtime_migrations (name, applied_at) VALUES (?, ?)', - args: [name, Date.now()], - }); + await applyRuntimeMigration(client, name, source); } } finally { client.close(); @@ -144,6 +140,8 @@ try { env: { ...runtimeEnv, HOST: '127.0.0.1', + NITRO_HOST: '127.0.0.1', + NITRO_PORT: '3020', PORT: '3020', }, stdio: ['ignore', 'pipe', 'pipe'], diff --git a/integrations/workbuddy/runtime/migrations.mjs b/integrations/workbuddy/runtime/migrations.mjs new file mode 100644 index 0000000..bd99759 --- /dev/null +++ b/integrations/workbuddy/runtime/migrations.mjs @@ -0,0 +1,23 @@ +/** Apply trusted packaged DDL and its receipt atomically. Runtime upgrades may + * already have added columns/tables before their first versioned migration. */ +export async function applyRuntimeMigration(client, name, source) { + const tx = await client.transaction('write'); + try { + for (const statement of source.split('--> statement-breakpoint').map((s) => s.trim()).filter(Boolean)) { + const addedColumn = /^ALTER TABLE\s+`(\w+)`\s+ADD\s+`(\w+)`\s/i.exec(statement); + if (addedColumn) { + const columns = await tx.execute(`PRAGMA table_info(\`${addedColumn[1]}\`)`); + if (columns.rows.some((row) => row.name === addedColumn[2])) continue; + } + // DDL is packaged maintainer SQL, never request input. Schema-aware repeats + // tolerate the same additive tables created by source-mode startup. + const sql = statement.replace(/^CREATE TABLE\s+(?!IF NOT EXISTS)/i, 'CREATE TABLE IF NOT EXISTS ').replace(/^CREATE (UNIQUE )?INDEX\s+(?!IF NOT EXISTS)/i, (_, unique = '') => `CREATE ${unique}INDEX IF NOT EXISTS `); + await tx.execute(sql); + } + await tx.execute({sql:'INSERT INTO __beatdesign_runtime_migrations (name, applied_at) VALUES (?, ?)',args:[name, Date.now()]}); + await tx.commit(); + } catch (error) { + await tx.rollback(); + throw error; + } +} diff --git a/messages/en.json b/messages/en.json index 835da92..000bf04 100644 --- a/messages/en.json +++ b/messages/en.json @@ -425,6 +425,19 @@ "materialStudy": "Material study", "surrealFashionFilm": "Surreal fashion film" } + }, + "textNode": { + "create": "Add text node", + "defaultName": "Text", + "title": "Text node title", + "role": "Text purpose", + "content": "Text content", + "note": "Note", + "script": "Script", + "character": "Character", + "storyboard": "Shot prompt", + "placeholder": "Write a script, character description, or shot prompt…", + "referenceHint": "Attach from a generation node’s reference picker" } }, "editor": { @@ -799,5 +812,143 @@ "setAsCover": "Set as cover", "coverSaved": "Project cover updated", "conflict": "Project settings changed elsewhere. Close and reopen settings before saving." + }, + "Organization": { + "groupLayout": "Group contents", + "arrangeGroups": "Arrange groups", + "arrangeTargets": "Arrange {count} groups. Select groups below to limit the scope.", + "expandArrangeAll": "Expand and arrange all groups", + "layoutHelp": "Arrangement changes layout only and supports Undo. Media, takes and Timeline stay intact.", + "selectLayoutGroup": "Select {name} for arrangement", + "layout_tidy": "Tidy up", + "layout_row": "Row", + "layout_column": "Column", + "layout_fit": "Fit contents", + "title": "Project organization", + "closePanel": "Close organization", + "localOnly": "Folders and reusable references stay in this local project.", + "folders": "Folders", + "elements": "Elements", + "conflict": "The project changed. Review the latest version before saving.", + "reviewLatest": "Review latest version", + "failed": "Could not save changes.", + "searchScope": "Search scope", + "allAssets": "Entire project", + "currentFolder": "Current folder", + "folder": "Folder", + "unclassified": "Unclassified", + "search": "Search assets", + "folderName": "Folder name", + "createFolder": "Create folder", + "rename": "Rename", + "removeFolder": "Remove folder…", + "destination": "Destination", + "moveFolder": "Move folder", + "moveSelected": "Move {count} selected assets", + "removalPreview": "Review folder removal", + "removalSummary": "{assets} assets and {folders} child folders move to {destination}.", + "nameConflicts": "Resolve conflicting folder names first.", + "confirmRemove": "Confirm removal", + "cancel": "Cancel", + "assets": "Assets", + "locate": "Locate", + "noAssets": "No assets in this scope.", + "loadMore": "Load more", + "element": "Element", + "newElement": "New Element", + "name": "Name", + "kind": "Kind", + "character": "Character", + "scene": "Scene", + "prop": "Prop", + "voice": "Voice", + "description": "Description", + "sourceAsset": "Source asset", + "chooseAsset": "Choose asset", + "save": "Save", + "createElement": "Create Element", + "addVersion": "Add version", + "versions": "Versions", + "versionHelp": "Versions pin concrete assets. Changing current does not update existing shots.", + "current": "Current", + "setCurrent": "Set current", + "removeElement": "Remove Element", + "removedElements": "Removed Elements", + "restore": "Restore", + "loading": "Loading…", + "retry": "Retry saved request", + "retryPending": "A request is awaiting confirmation. Retry it before making another change.", + "templates": "Workflow templates", + "templateLocalOnly": "Saved locally. Insertions create editable drafts and never run generation.", + "templateSave": "Save as template", + "templateName": "Template name", + "templatePreviewSelection": "Preview selected steps", + "templateStructure": "{steps} steps · {references} reference slots · {connections} connections", + "templateLibrary": "Local template library", + "templateChoose": "Choose template", + "templateRequired": "Required", + "templateUnbound": "Unbound", + "templateDraftUnbound": "Unbound required slots block generation. You can bind them later in this draft.", + "templateInsert": "Insert editable draft", + "templateRename": "Rename template", + "templateRemove": "Remove template…", + "templateRemoveImpact": "Existing instances remain unchanged.", + "templateConfirmRemove": "Confirm remove template", + "templateCancel": "Cancel", + "canvasOrganization": "Organize Canvas", + "groups": "Groups", + "groupName": "Group name", + "groupRole": "Group role", + "shot": "Shot", + "references": "References", + "sound": "Sound", + "packaging": "Packaging", + "timeline": "Timeline", + "previewGroup": "Preview group from selection", + "groupPreview": "{count} nodes will join this group.", + "createGroup": "Create group", + "chooseNodes": "Select nodes with saved positions first.", + "showHistory": "Show all versions", + "showAllReferences": "Show all reference connections", + "expand": "Expand", + "collapse": "Collapse", + "ungroup": "Ungroup", + "useSelection": "Replace members with selection", + "selectConsumer": "Select one generation step to bind an Element.", + "inputRole": "Input role", + "input_reference": "Reference", + "input_subject": "Subject", + "input_style": "Style", + "input_pose": "Pose", + "input_source": "Source", + "input_audio_track": "Audio track", + "bindElement": "Bind fixed version", + "needsReview": "A newer current version is available. Review this input explicitly.", + "keepVersion": "Keep this version", + "adoptVersion": "Adopt current version", + "unbind": "Unbind", + "timelineUsage": "View Timeline usage", + "activeTake": "Active take", + "templateInputs": "Template instance inputs", + "detachTemplate": "Detach template", + "historyInspector": "All generation versions", + "historyReadOnly": "Previewing a version does not change the displayed result or Timeline.", + "displayedResult": "Displayed result", + "historyEmpty": "No generation versions yet.", + "ready": "Ready", + "noResult": "No displayed result selected", + "timelineUses": "Timeline uses {count}", + "reviewNotRecorded": "Review not recorded", + "unavailableModel": "Choose an available model before generating.", + "timelineRange": "Timeline {start}s · {duration}s; source {sourceIn}–{sourceOut}s", + "caption": "Caption", + "hidden": "Hidden", + "muted": "Muted", + "previewAdopted": "Preview adopted source", + "templatePlacement": "Insertion position", + "templatePlacementX": "Canvas X", + "templatePlacementY": "Canvas Y", + "referenceConsumers": "Consumers", + "referencePosition": "Input {position}" } } diff --git a/messages/ja.json b/messages/ja.json index f805638..9732f0b 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -425,6 +425,19 @@ "materialStudy": "素材のスタディ", "surrealFashionFilm": "シュールなファッション映像" } + }, + "textNode": { + "create": "テキストノードを追加", + "defaultName": "テキスト", + "title": "テキストのタイトル", + "role": "テキストの用途", + "content": "テキスト内容", + "note": "メモ", + "script": "脚本", + "character": "キャラクター設定", + "storyboard": "ショット指示", + "placeholder": "脚本、キャラクター設定、ショット指示を記入…", + "referenceHint": "生成ノードの参照からこのテキストを選択" } }, "editor": { @@ -799,5 +812,143 @@ "setAsCover": "カバーに設定", "coverSaved": "カバーを更新しました", "conflict": "設定が別の場所で更新されました。閉じて再度開いてから保存してください。" + }, + "Organization": { + "groupLayout": "Group contents", + "arrangeGroups": "Arrange groups", + "arrangeTargets": "Arrange {count} groups. Select groups below to limit the scope.", + "expandArrangeAll": "Expand and arrange all groups", + "layoutHelp": "Arrangement changes layout only and supports Undo. Media, takes and Timeline stay intact.", + "selectLayoutGroup": "Select {name} for arrangement", + "layout_tidy": "Tidy up", + "layout_row": "Row", + "layout_column": "Column", + "layout_fit": "Fit contents", + "title": "Project organization", + "closePanel": "Close organization", + "localOnly": "Folders and reusable references stay in this local project.", + "folders": "Folders", + "elements": "Elements", + "conflict": "The project changed. Review the latest version before saving.", + "reviewLatest": "Review latest version", + "failed": "Could not save changes.", + "searchScope": "Search scope", + "allAssets": "Entire project", + "currentFolder": "Current folder", + "folder": "Folder", + "unclassified": "Unclassified", + "search": "Search assets", + "folderName": "Folder name", + "createFolder": "Create folder", + "rename": "Rename", + "removeFolder": "Remove folder…", + "destination": "Destination", + "moveFolder": "Move folder", + "moveSelected": "Move {count} selected assets", + "removalPreview": "Review folder removal", + "removalSummary": "{assets} assets and {folders} child folders move to {destination}.", + "nameConflicts": "Resolve conflicting folder names first.", + "confirmRemove": "Confirm removal", + "cancel": "Cancel", + "assets": "Assets", + "locate": "Locate", + "noAssets": "No assets in this scope.", + "loadMore": "Load more", + "element": "Element", + "newElement": "New Element", + "name": "Name", + "kind": "Kind", + "character": "Character", + "scene": "Scene", + "prop": "Prop", + "voice": "Voice", + "description": "Description", + "sourceAsset": "Source asset", + "chooseAsset": "Choose asset", + "save": "Save", + "createElement": "Create Element", + "addVersion": "Add version", + "versions": "Versions", + "versionHelp": "Versions pin concrete assets. Changing current does not update existing shots.", + "current": "Current", + "setCurrent": "Set current", + "removeElement": "Remove Element", + "removedElements": "Removed Elements", + "restore": "Restore", + "loading": "Loading…", + "retry": "Retry saved request", + "retryPending": "A request is awaiting confirmation. Retry it before making another change.", + "templates": "Workflow templates", + "templateLocalOnly": "Saved locally. Insertions create editable drafts and never run generation.", + "templateSave": "Save as template", + "templateName": "Template name", + "templatePreviewSelection": "Preview selected steps", + "templateStructure": "{steps} steps · {references} reference slots · {connections} connections", + "templateLibrary": "Local template library", + "templateChoose": "Choose template", + "templateRequired": "Required", + "templateUnbound": "Unbound", + "templateDraftUnbound": "Unbound required slots block generation. You can bind them later in this draft.", + "templateInsert": "Insert editable draft", + "templateRename": "Rename template", + "templateRemove": "Remove template…", + "templateRemoveImpact": "Existing instances remain unchanged.", + "templateConfirmRemove": "Confirm remove template", + "templateCancel": "Cancel", + "canvasOrganization": "Organize Canvas", + "groups": "Groups", + "groupName": "Group name", + "groupRole": "Group role", + "shot": "Shot", + "references": "References", + "sound": "Sound", + "packaging": "Packaging", + "timeline": "Timeline", + "previewGroup": "Preview group from selection", + "groupPreview": "{count} nodes will join this group.", + "createGroup": "Create group", + "chooseNodes": "Select nodes with saved positions first.", + "showHistory": "Show all versions", + "showAllReferences": "Show all reference connections", + "expand": "Expand", + "collapse": "Collapse", + "ungroup": "Ungroup", + "useSelection": "Replace members with selection", + "selectConsumer": "Select one generation step to bind an Element.", + "inputRole": "Input role", + "input_reference": "Reference", + "input_subject": "Subject", + "input_style": "Style", + "input_pose": "Pose", + "input_source": "Source", + "input_audio_track": "Audio track", + "bindElement": "Bind fixed version", + "needsReview": "A newer current version is available. Review this input explicitly.", + "keepVersion": "Keep this version", + "adoptVersion": "Adopt current version", + "unbind": "Unbind", + "timelineUsage": "View Timeline usage", + "activeTake": "Active take", + "templateInputs": "Template instance inputs", + "detachTemplate": "Detach template", + "historyInspector": "All generation versions", + "historyReadOnly": "Previewing a version does not change the displayed result or Timeline.", + "displayedResult": "Displayed result", + "historyEmpty": "No generation versions yet.", + "ready": "Ready", + "noResult": "No displayed result selected", + "timelineUses": "Timeline uses {count}", + "reviewNotRecorded": "Review not recorded", + "unavailableModel": "Choose an available model before generating.", + "timelineRange": "Timeline {start}s · {duration}s; source {sourceIn}–{sourceOut}s", + "caption": "Caption", + "hidden": "Hidden", + "muted": "Muted", + "previewAdopted": "Preview adopted source", + "templatePlacement": "Insertion position", + "templatePlacementX": "Canvas X", + "templatePlacementY": "Canvas Y", + "referenceConsumers": "Consumers", + "referencePosition": "Input {position}" } } diff --git a/messages/zh.json b/messages/zh.json index 6f7b2e5..b119e05 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -425,6 +425,19 @@ "materialStudy": "材质探索", "surrealFashionFilm": "超现实时尚短片" } + }, + "textNode": { + "create": "添加文本节点", + "defaultName": "文本", + "title": "文本节点标题", + "role": "文本用途", + "content": "文本内容", + "note": "笔记", + "script": "剧本", + "character": "角色设定", + "storyboard": "分镜提示词", + "placeholder": "在这里编写剧本、角色设定或分镜提示词…", + "referenceHint": "在生成节点的参考选择器中引用此文本" } }, "editor": { @@ -799,5 +812,143 @@ "setAsCover": "设为项目封面", "coverSaved": "项目封面已更新", "conflict": "项目设置已在其他位置更新。请关闭后重新打开,再保存更改。" + }, + "Organization": { + "groupLayout": "组内排列", + "arrangeGroups": "组间排列", + "arrangeTargets": "排列 {count} 个组;可在下方勾选组以缩小范围。", + "expandArrangeAll": "展开并整理全部组", + "layoutHelp": "排列只调整位置,支持撤销;素材、旧版本和时间线保持不变。", + "selectLayoutGroup": "选择 {name} 参与排列", + "layout_tidy": "整理", + "layout_row": "横排", + "layout_column": "竖排", + "layout_fit": "适应内容", + "title": "项目组织", + "closePanel": "关闭项目组织", + "localOnly": "文件夹和可复用参考保存在本地项目中。", + "folders": "文件夹", + "elements": "共享参考", + "conflict": "项目已变化,请复核最新版本后保存。", + "reviewLatest": "复核最新版本", + "failed": "无法保存修改。", + "searchScope": "搜索范围", + "allAssets": "整个项目", + "currentFolder": "当前文件夹", + "folder": "文件夹", + "unclassified": "未分类", + "search": "搜索素材", + "folderName": "文件夹名称", + "createFolder": "新建文件夹", + "rename": "重命名", + "removeFolder": "移除文件夹…", + "destination": "目标位置", + "moveFolder": "移动文件夹", + "moveSelected": "移动已选的 {count} 个素材", + "removalPreview": "预览文件夹移除影响", + "removalSummary": "{assets} 个素材和 {folders} 个子文件夹将提升至 {destination}。", + "nameConflicts": "请先处理重名文件夹。", + "confirmRemove": "确认移除", + "cancel": "取消", + "assets": "素材", + "locate": "定位", + "noAssets": "该范围内没有素材。", + "loadMore": "加载更多", + "element": "共享参考", + "newElement": "新建共享参考", + "name": "名称", + "kind": "类型", + "character": "角色", + "scene": "场景", + "prop": "道具", + "voice": "声音", + "description": "描述", + "sourceAsset": "源素材", + "chooseAsset": "选择素材", + "save": "保存", + "createElement": "创建共享参考", + "addVersion": "添加版本", + "versions": "版本", + "versionHelp": "每个版本固定到具体素材。更换当前版本不会自动更新已有镜头。", + "current": "当前版本", + "setCurrent": "设为当前版本", + "removeElement": "移除共享参考", + "removedElements": "已移除的共享参考", + "restore": "恢复", + "loading": "加载中…", + "retry": "重试原请求", + "retryPending": "一个请求的结果尚未确认,请先重试原请求。", + "templates": "工作流模板", + "templateLocalOnly": "模板保存在本机。插入后创建可编辑草稿,不会自动运行生成。", + "templateSave": "另存为模板", + "templateName": "模板名称", + "templatePreviewSelection": "预览所选步骤", + "templateStructure": "{steps} 个步骤 · {references} 个参考槽 · {connections} 条连接", + "templateLibrary": "本机模板库", + "templateChoose": "选择模板", + "templateRequired": "必填", + "templateUnbound": "未绑定", + "templateDraftUnbound": "必填参考槽未绑定时无法生成,可在草稿中继续绑定。", + "templateInsert": "插入可编辑草稿", + "templateRename": "重命名模板", + "templateRemove": "移除模板…", + "templateRemoveImpact": "已创建的实例保持原样。", + "templateConfirmRemove": "确认移除模板", + "templateCancel": "取消", + "canvasOrganization": "组织画布", + "groups": "节点组", + "groupName": "组名称", + "groupRole": "组用途", + "shot": "镜头", + "references": "参考", + "sound": "声音", + "packaging": "包装", + "timeline": "时间线", + "previewGroup": "预览所选节点分组", + "groupPreview": "此组将包含 {count} 个节点。", + "createGroup": "建立组", + "chooseNodes": "请先选择已有位置的节点。", + "showHistory": "显示所有版本", + "showAllReferences": "显示全部参考连线", + "expand": "展开", + "collapse": "折叠", + "ungroup": "解除分组", + "useSelection": "将所选节点设为组成员", + "selectConsumer": "选择一个生成步骤以绑定共享参考。", + "inputRole": "输入用途", + "input_reference": "参考", + "input_subject": "主体", + "input_style": "风格", + "input_pose": "姿态", + "input_source": "源输入", + "input_audio_track": "音轨", + "bindElement": "绑定固定版本", + "needsReview": "当前版本已变化,请明确复核此输入。", + "keepVersion": "保留此版本", + "adoptVersion": "采用当前版本", + "unbind": "解除绑定", + "timelineUsage": "查看时间线实际采用", + "activeTake": "采用的 Take", + "templateInputs": "模板实例参考槽", + "detachTemplate": "解除模板关联", + "historyInspector": "全部生成版本", + "historyReadOnly": "预览版本不会改变当前展示结果或时间线采用。", + "displayedResult": "当前展示结果", + "historyEmpty": "暂无生成版本。", + "ready": "已完成", + "noResult": "未指定展示结果", + "timelineUses": "时间线采用 {count} 次", + "reviewNotRecorded": "未记录人工审核", + "unavailableModel": "请先选择当前可用的模型。", + "timelineRange": "时间线 {start} 秒 · 时长 {duration} 秒;源区间 {sourceIn}–{sourceOut} 秒", + "caption": "字幕", + "hidden": "已隐藏", + "muted": "已静音", + "previewAdopted": "预览实际采用素材", + "templatePlacement": "插入位置", + "templatePlacementX": "画布 X 坐标", + "templatePlacementY": "画布 Y 坐标", + "referenceConsumers": "使用者", + "referencePosition": "输入 {position}" } } diff --git a/package.json b/package.json index 6bc84b0..b304e81 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "mcp:http": "tsx scripts/mcp-server.ts --http", "prebuild": "node scripts/db-setup.mjs", "build": "tsx scripts/prepare-paraglide.ts && vite build && node scripts/sanitize-output.mjs", - "start": "node .output/server/index.mjs", + "start": "node scripts/start-local.mjs", "postinstall": "node scripts/postinstall.mjs", "db:setup": "node scripts/db-setup.mjs", "db:push": "drizzle-kit push --config=drizzle.config.ts", diff --git a/playwright.config.ts b/playwright.config.ts index 873a83d..88bc549 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,7 +5,15 @@ export default defineConfig({ workers: 1, timeout: 90_000, expect: { timeout: 15_000 }, - use: { baseURL: 'http://127.0.0.1:3042', viewport: { width: 1440, height: 960 }, screenshot: 'only-on-failure', trace: 'retain-on-failure' }, + use: { + baseURL: 'http://127.0.0.1:3042', + viewport: { width: 1440, height: 960 }, + screenshot: 'only-on-failure', + trace: 'retain-on-failure', + ...(process.platform === 'win32' + ? { launchOptions: { channel: 'chrome' } } + : {}), + }, webServer: { command: 'node scripts/start-e2e.mjs', url: 'http://127.0.0.1:3042/api/ping', diff --git a/scripts/build-workbuddy-runtime-package.ts b/scripts/build-workbuddy-runtime-package.ts index 8b2e698..0cf460d 100644 --- a/scripts/build-workbuddy-runtime-package.ts +++ b/scripts/build-workbuddy-runtime-package.ts @@ -95,6 +95,7 @@ await Promise.all([ resolve(repositoryRoot, 'integrations', 'workbuddy', 'runtime', 'launcher.mjs'), resolve(outputRoot, 'bin', 'beatdesign-workbuddy.mjs') ), + cp(resolve(repositoryRoot, 'integrations', 'workbuddy', 'runtime', 'migrations.mjs'), resolve(outputRoot, 'bin', 'migrations.mjs')), cp( resolve(repositoryRoot, 'integrations', 'workbuddy', 'runtime', 'README.md'), resolve(outputRoot, 'README.md') diff --git a/scripts/seed-organization-fixture.ts b/scripts/seed-organization-fixture.ts new file mode 100644 index 0000000..f98e3e5 --- /dev/null +++ b/scripts/seed-organization-fixture.ts @@ -0,0 +1,408 @@ +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + createProject, + loadProjectWithLatestSnapshot, +} from '@/core/projects/projects'; +import { persistLocalProjectAsset } from '@/core/projects/local-project-assets'; +import { + recordUserAsset, + linkProjectAsset, +} from '@/core/workspace-lib/assets/user-assets'; +import { persistBeatDesignCommand } from '@/core/commands/persist'; +import { canvasCardSchema } from '@/core/commands/schema'; +import { timelineCanvasCardId } from '@/core/commands/canvas-commands'; +import { loadProjectTimeline } from '@/core/editor/timeline-state'; +import { projectCanvasOrganization } from '@/core/organization/canvas-projection'; +import { getTimelineUsage } from '@/core/editor/timeline-usage'; +import { getDb } from '@/core/workspace-lib/db-adapter'; +import type { CanvasOperation } from '@/core/commands/canvas-commands'; +import type { EditorOperation } from '@/core/commands/editor-commands'; + +const pixel = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAJCAIAAAC0SDtlAAAAE0lEQVR42mPQ96slCTGMahgUGgD8sYyh+gdn4AAAAABJRU5ErkJggg==', + 'base64' +); +function silence() { + const bytes = Buffer.alloc(1644); + bytes.write('RIFF'); + bytes.writeUInt32LE(bytes.length - 8, 4); + bytes.write('WAVEfmt ', 8); + bytes.writeUInt32LE(16, 16); + bytes.writeUInt16LE(1, 20); + bytes.writeUInt16LE(1, 22); + bytes.writeUInt32LE(8000, 24); + bytes.writeUInt32LE(16000, 28); + bytes.writeUInt16LE(2, 32); + bytes.writeUInt16LE(16, 34); + bytes.write('data', 36); + bytes.writeUInt32LE(bytes.length - 44, 40); + return bytes; +} + +/** Synthetic-only demonstration. Never defaults to a user's active data directory. */ +export async function seedOrganizationFixture() { + const configured = process.env.BEATDESIGN_DATA_DIR?.trim(); + if (!configured || resolve(configured) === resolve('data')) + throw new Error( + 'Set BEATDESIGN_DATA_DIR to an explicitly isolated fixture/preview directory.' + ); + const directory = resolve(configured); + await mkdir(directory, { recursive: true }); + const project = await createProject({ + name: 'Canvas organization · 8 shot synthetic preview', + }); + const target = await createProject({ + name: 'Template binding · isolated target', + }); + const assetIds: string[] = []; + const assetUrls = new Map(); + const hashes: Record = {}; + async function asset( + index: string, + type: 'image' | 'video' | 'audio', + bytes: Buffer + ) { + const assetId = `${project.id}-${index}`; + const suffix = type === 'image' ? 'png' : type === 'video' ? 'mp4' : 'wav'; + const mimeType = + type === 'image' + ? 'image/png' + : type === 'video' + ? 'video/mp4' + : 'audio/wav'; + const persisted = await persistLocalProjectAsset({ + projectId: project.id, + assetId, + filename: `Synthetic-${index}.${suffix}`, + mimeType, + bytes, + }); + await recordUserAsset({ + id: assetId, + type, + source: 'upload', + bucket: 'local-project-assets', + objectKey: persisted.objectKey, + publicUrl: persisted.publicUrl, + filename: persisted.filename, + mimeType, + sizeBytes: persisted.sizeBytes, + sha256: persisted.sha256, + storageProvider: 'local', + originProjectId: project.id, + metadata: { syntheticFixture: true }, + ...(type === 'image' + ? { width: 16, height: 9 } + : { durationMs: type === 'audio' ? 100 : 1000 }), + }); + await linkProjectAsset({ projectId: project.id, assetId, role: 'upload' }); + assetIds.push(assetId); + assetUrls.set(assetId, persisted.publicUrl); + hashes[assetId] = persisted.sha256; + return assetId; + } + const images: string[] = []; + for (let i = 0; i < 502; i++) + images.push( + await asset(`image-${String(i).padStart(3, '0')}`, 'image', pixel) + ); + const videoBytes = await readFile(resolve('tests/fixtures/tiny-black.mp4')); + const oldVideo = await asset('video-old', 'video', videoBytes); + const newVideo = await asset('video-new', 'video', videoBytes); + const voice = await asset('voice', 'audio', silence()); + // Multiple classifications exercise distinct Asset identity rather than membership identity. + await linkProjectAsset({ + projectId: project.id, + assetId: images[0], + role: 'reference', + }); + const editorOperations: EditorOperation[] = []; + for (let shot = 1; shot <= 8; shot++) { + const clipId = `shot-${shot}-clip`; + const baseAssetId = shot === 1 ? oldVideo : newVideo; + editorOperations.push( + { + type: 'add_clip', + clipId, + assetId: baseAssetId, + sourceUrl: assetUrls.get(baseAssetId)!, + name: `Shot ${shot}`, + sourceType: 'video', + sourceDuration: 1, + startTime: (shot - 1) * 0.2, + }, + { + type: 'add_take', + clipId, + take: { + id: `shot-${shot}-old-take`, + assetId: oldVideo, + sourceUrl: assetUrls.get(oldVideo)!, + name: 'Earlier chosen take', + sourceDuration: 1, + sourceGenerationId: null, + prompt: 'Synthetic fixture; no model job', + createdAt: '2026-10-20T00:00:00.000Z', + }, + }, + { type: 'trim_clip', clipId, inPoint: 0.1, outPoint: 0.3 } + ); + if (shot > 1) + editorOperations.push({ + type: 'activate_take', + clipId, + takeId: `shot-${shot}-old-take`, + }); + } + const edit = await persistBeatDesignCommand({ + projectId: project.id, + origin: 'system', + commandId: `fixture-editor-${project.id}`, + command: { type: 'editor.apply', operations: editorOperations }, + }); + if (!edit.ok) throw new Error(edit.message); + const timeline = (await loadProjectTimeline(project.id))!; + const timelineCardId = timelineCanvasCardId(timeline.document.id); + const canvasOperations: CanvasOperation[] = []; + const support = [ + { + id: 'support-character', + name: 'Shared character', + type: 'image' as const, + assetId: images[0], + }, + { + id: 'support-location', + name: 'Shared location', + type: 'image' as const, + assetId: images[1], + }, + { + id: 'support-voice', + name: 'Shared voice', + type: 'audio' as const, + assetId: voice, + }, + ]; + for (let i = 0; i < support.length; i++) + canvasOperations.push({ + type: 'upsert_card', + card: canvasCardSchema.parse({ + ...support[i], + kind: 'asset', + aspectRatio: '16:9', + url: assetUrls.get(support[i].assetId), + }), + frame: { x: 180 + i * 390, y: 850, w: 320, h: 180 }, + }); + canvasOperations.push({ + type: 'upsert_timeline_node', + timelineId: timeline.document.id, + name: 'Actual Timeline usage', + durationSec: timeline.document.duration, + clipCount: 8, + }); + for (let shot = 1; shot <= 8; shot++) { + const prefix = `shot-${shot}`; + const x = 180 + ((shot - 1) % 4) * 390; + const y = 160 + Math.floor((shot - 1) / 4) * 310; + const memberCardIds: string[] = []; + const script = canvasCardSchema.parse({ + id: `${prefix}-script`, + kind: 'text', + type: 'text', + name: `Shot ${shot} intention`, + textContent: + 'Synthetic motion and composition example. No story adaptation or model generation.', + textRole: 'storyboard', + }); + const image = canvasCardSchema.parse({ + id: `${prefix}-image`, + kind: 'generation', + type: 'image', + name: `Shot ${shot} composition`, + modelId: 'gpt-image-2.5-flare', + prompt: 'Synthetic blue frame', + referenceCardIds: ['support-character', 'support-location', script.id], + pinnedOutputId: `${prefix}-image-output-6`, + }); + const motion = canvasCardSchema.parse({ + id: `${prefix}-motion`, + kind: 'generation', + type: 'video', + name: `Shot ${shot} motion`, + modelId: 'seedance-2.0', + prompt: 'Synthetic motion fixture', + referenceCardIds: [`${prefix}-image-output-6`, 'support-character'], + pinnedOutputId: `${prefix}-video-output-6`, + }); + const cards = [script, image, motion]; + for (const source of [image, motion]) + for (let take = 0; take < 7; take++) { + const media = + source.type === 'image' + ? images[2 + take] + : take === 0 + ? oldVideo + : newVideo; + cards.push( + canvasCardSchema.parse({ + ...source, + id: `${prefix}-${source.type}-output-${take}`, + kind: 'output', + name: `Shot ${shot} ${source.type} take ${take + 1}`, + assetId: media, + url: assetUrls.get(media), + sourceConfigCardId: source.id, + generationRunId: `synthetic-${prefix}-${source.type}-${take}`, + status: 'succeeded', + generationSnapshot: { + type: source.type, + prompt: source.prompt, + referenceCardIds: source.referenceCardIds, + modelId: source.modelId, + capturedAt: `2026-10-20T00:00:0${take}.000Z`, + }, + }) + ); + } + for (let i = 0; i < cards.length; i++) { + memberCardIds.push(cards[i].id); + canvasOperations.push({ + type: 'upsert_card', + card: cards[i], + frame: { + x: x + (i % 3) * 350, + y: y + Math.floor(i / 3) * 220, + w: 320, + h: 180, + }, + }); + } + canvasOperations.push({ + type: 'create_group', + groupId: prefix, + name: `Shot ${String(shot).padStart(2, '0')}`, + role: 'shot', + memberCardIds, + collapsed: true, + resultCardId: motion.id, + summaryFrame: { x, y, w: 340, h: 210 }, + expandedFrame: { x, y, w: 1060, h: 1360 }, + }); + } + // Keep the standalone Timeline entry aligned with the other shared controls. + canvasOperations.push({ + type: 'move_card', + cardId: timelineCardId, + frame: { x: 1350, y: 850, w: 320, h: 180 }, + }); + const canvas = await persistBeatDesignCommand({ + projectId: project.id, + origin: 'system', + commandId: `fixture-canvas-${project.id}`, + idempotencyKey: `fixture-canvas-${project.id}`, + expectedRevision: 1, + command: { type: 'canvas.apply', operations: canvasOperations }, + }); + if (!canvas.ok) throw new Error(canvas.message); + const organized = await persistBeatDesignCommand({ + projectId: project.id, + origin: 'system', + commandId: `fixture-org-${project.id}`, + idempotencyKey: `fixture-org-${project.id}`, + expectedRevision: 0, + command: { + type: 'organization.apply', + operations: [ + { + type: 'create_folder', + folderId: 'reference-library', + name: 'References', + parentId: null, + }, + { + type: 'create_folder', + folderId: 'old-takes', + name: 'Earlier takes', + parentId: null, + }, + { + type: 'assign_assets', + folderId: 'reference-library', + assetIds: images, + }, + { type: 'assign_assets', folderId: 'old-takes', assetIds: [oldVideo] }, + { + type: 'create_element', + elementId: 'character', + name: 'Synthetic character', + kind: 'character', + description: 'Synthetic reference for UI tests', + versionId: 'v1', + assetId: images[0], + }, + { + type: 'add_element_version', + elementId: 'character', + versionId: 'v2', + assetId: images[1], + }, + ], + }, + }); + if (!organized.ok) throw new Error(organized.message); + const final = (await loadProjectWithLatestSnapshot({ + projectId: project.id, + }))!; + const projection = projectCanvasOrganization(final.snapshot); + if (final.snapshot.cards.length !== 140 || projection.nodes.length !== 12) + throw new Error( + `Fixture size mismatch: ${final.snapshot.cards.length} records, ${projection.nodes.length} entries` + ); + const manifest = { + syntheticOnly: true, + projectId: project.id, + targetProjectId: target.id, + cards: 140, + groups: 8, + projectedEntries: 12, + assetCount: assetIds.length, + assetIds, + imageAssetId: images[0], + secondImageAssetId: images[1], + oldVideoAssetId: oldVideo, + newVideoAssetId: newVideo, + timelineCardId, + hashes, + timelineUsage: getTimelineUsage(timeline.document, timeline.version), + }; + await writeFile( + resolve(directory, 'organization-fixture.json'), + `${JSON.stringify(manifest, null, 2)}\n` + ); + return manifest; +} + +if ( + process.argv[1] && + resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const isolatedDirectory = process.env.BEATDESIGN_DATA_DIR?.trim(); + try { + const manifest = await seedOrganizationFixture(); + console.log( + JSON.stringify({ + fixture: true, + projectId: manifest.projectId, + cards: manifest.cards, + projectedEntries: manifest.projectedEntries, + }) + ); + } finally { + if (isolatedDirectory && resolve(isolatedDirectory) !== resolve('data')) + (await getDb()).$client.close(); + } +} diff --git a/scripts/start-e2e.mjs b/scripts/start-e2e.mjs index 74dd58e..c7cca4e 100644 --- a/scripts/start-e2e.mjs +++ b/scripts/start-e2e.mjs @@ -11,6 +11,7 @@ try { const directory = await mkdtemp(join(tmpdir(), 'beatdesign-e2e-')); const env = { ...process.env, BEATDESIGN_DATA_DIR: directory, NITRO_HOST: '127.0.0.1', NITRO_PORT: '3042', BEATDESIGN_MCP_HTTP_PORT: '3043', BEATDESIGN_MCP_TOKEN: 'e2e-local-only' }; execFileSync('pnpm', ['db:push'], { env, stdio: 'inherit' }); +execFileSync(process.execPath, ['--import', 'tsx', 'scripts/seed-organization-fixture.ts'], {env, stdio:'inherit'}); const children = [ // Exercise the distributed workspace rather than Vite's HMR/error overlay. spawn(process.execPath, ['.output/server/index.mjs'], { env, stdio: 'inherit' }), diff --git a/scripts/start-local.mjs b/scripts/start-local.mjs new file mode 100644 index 0000000..2482626 --- /dev/null +++ b/scripts/start-local.mjs @@ -0,0 +1,6 @@ +// The core workspace has no multi-user authentication; always listen on loopback. +// Set both Nitro and generic variables so ambient hosting defaults cannot widen it. +process.env.HOST = '127.0.0.1'; +process.env.NITRO_HOST = '127.0.0.1'; +process.env.NITRO_PORT = process.env.NITRO_PORT || process.env.PORT || '3020'; +await import('../.output/server/index.mjs'); diff --git a/src/components/app/project-assets-workspace.tsx b/src/components/app/project-assets-workspace.tsx index 89c8a8d..fee7936 100644 --- a/src/components/app/project-assets-workspace.tsx +++ b/src/components/app/project-assets-workspace.tsx @@ -1,3 +1,4 @@ +import { ProjectOrganizationWorkspace } from './project-organization-workspace'; import { ProjectAssetsLibrary } from '@/components/app/project-assets-dialog'; import { useTranslations } from '@/core/workspace-lib/shims/next-intl'; @@ -15,6 +16,7 @@ export function ProjectAssetsWorkspace({ projectId }: { projectId: string }) {

{t('description')}

+
diff --git a/src/components/app/project-organization-panel.tsx b/src/components/app/project-organization-panel.tsx new file mode 100644 index 0000000..ba119b9 --- /dev/null +++ b/src/components/app/project-organization-panel.tsx @@ -0,0 +1,863 @@ +import { useEffect, useRef, useState } from 'react'; +import { useTranslations } from '@/core/workspace-lib/shims/next-intl'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { + previewFolderRemoval, + type OrganizationOperation, + type ProjectOrganizationDocument, + type AssetFolder, +} from '@/core/organization/organization-document'; + +export type OrganizationPanelAsset = { + id: string; + type: string; + filename?: string | null; + publicUrl?: string | null; +}; +export type OrganizationAssetQuery = { + folderId: string | null; + scope: 'current' | 'project'; + query: string; + cursor?: string; +}; +export type ProjectOrganizationPanelProps = { + document: ProjectOrganizationDocument; + revision: number; + assets: OrganizationPanelAsset[]; + onApply: ( + operations: OrganizationOperation[], + expectedRevision?: number + ) => Promise | unknown; + onQuery?: (query: OrganizationAssetQuery) => Promise<{ + assets: OrganizationPanelAsset[]; + nextCursor?: string | null; + } | void>; + onLocate?: (assetId: string) => void; + consumers?: Array<{ + elementId: string; + consumerCardId: string; + name: string; + }>; + onLocateConsumer?: (cardId: string) => void; +}; + +const control = + 'min-h-11 rounded-lg border border-white/15 bg-[var(--beat-surface,#17181b)] px-3 text-sm text-[var(--beat-text-1)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-orange-500 disabled:opacity-40'; +const button = `${control} hover:bg-white/10`; + +/** Project-local organization controls; all writes are delegated to the shared kernel owner. */ +export function ProjectOrganizationPanel({ + document, + revision, + assets, + onApply, + onQuery, + onLocate, + consumers, + onLocateConsumer, +}: ProjectOrganizationPanelProps) { + const t = useTranslations('Organization'); + const [tab, setTab] = useState<'folders' | 'elements'>('folders'); + const [folderId, setFolderId] = useState(null); + const [scope, setScope] = useState<'current' | 'project'>('project'); + const [query, setQuery] = useState(''); + const [queried, setQueried] = useState(null); + const [cursor, setCursor] = useState(null); + const [selected, setSelected] = useState([]); + const [folderName, setFolderName] = useState(''); + const [destination, setDestination] = useState(''); + const [preview, setPreview] = useState | null>(null); + const [elementId, setElementId] = useState(''); + const [elementName, setElementName] = useState(''); + const [description, setDescription] = useState(''); + const [kind, setKind] = useState<'character' | 'scene' | 'prop' | 'voice'>( + 'character' + ); + const [assetId, setAssetId] = useState(''); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + const [editRevision, setEditRevision] = useState(null); + const [narrow, setNarrow] = useState(false); + const [drawerOpen, setDrawerOpen] = useState(false); + const queryGeneration = useRef(0); + const folders = Object.values(document.folders).sort((a, b) => + a.name.localeCompare(b.name) + ); + const activeElements = Object.values(document.elements).filter( + (element) => !element.deletedAt + ); + const deletedElements = Object.values(document.elements).filter( + (element) => element.deletedAt + ); + const currentElement = elementId ? document.elements[elementId] : undefined; + const stale = editRevision !== null && editRevision !== revision; + const markEdit = () => setEditRevision((current) => current ?? revision); + + useEffect(() => { + const media = window.matchMedia('(max-width: 639px)'); + const update = () => setNarrow(media.matches); + update(); + media.addEventListener('change', update); + return () => media.removeEventListener('change', update); + }, []); + + useEffect(() => { + setQueried(null); + setCursor(null); + }, [assets]); + useEffect(() => { + if (editRevision !== null) return; + if (folderId && !document.folders[folderId]) { + setFolderId(null); + setScope('current'); + setQueried(null); + setCursor(null); + } + if ( + elementId && + (!document.elements[elementId] || document.elements[elementId].deletedAt) + ) { + setElementId(''); + setElementName(''); + setDescription(''); + setAssetId(''); + } + }, [document, folderId, elementId, editRevision]); + function folderPath(id: string): string { + const parts: string[] = []; + const seen = new Set(); + let current: string | null = id; + while (current && !seen.has(current)) { + seen.add(current); + const folder: AssetFolder | undefined = document.folders[current]; + if (!folder) break; + parts.unshift(folder.name); + current = folder.parentId; + } + return parts.join(' / '); + } + async function apply(operations: OrganizationOperation[]) { + if (busy) return; + if (stale) { + setError(t('conflict')); + return; + } + setBusy(true); + setError(''); + try { + await onApply(operations, editRevision ?? revision); + setEditRevision(null); + setPreview(null); + setSelected([]); + setQueried(null); + setCursor(null); + } catch (cause) { + setError(cause instanceof Error ? cause.message : t('failed')); + } finally { + setBusy(false); + } + } + async function search(next: OrganizationAssetQuery, append = false) { + if (!onQuery) return; + const generation = ++queryGeneration.current; + setBusy(true); + setError(''); + try { + const result = await onQuery(next); + if (generation !== queryGeneration.current || !result) return; + setQueried((previous) => + append + ? [ + ...new Map( + [...(previous ?? []), ...result.assets].map((asset) => [ + asset.id, + asset, + ]) + ).values(), + ] + : result.assets + ); + setCursor(result.nextCursor ?? null); + } catch (cause) { + if (generation === queryGeneration.current) + setError(cause instanceof Error ? cause.message : t('failed')); + } finally { + if (generation === queryGeneration.current) setBusy(false); + } + } + function pickElement(id: string) { + const element = document.elements[id]; + setElementId(id); + setElementName(element?.name ?? ''); + setDescription(element?.description ?? ''); + setKind(element?.kind ?? 'character'); + setAssetId(''); + setEditRevision(null); + setError(''); + } + const visibleAssets = + queried ?? + assets.filter( + (asset) => + (scope === 'project' || + (document.assetLocations[asset.id] ?? null) === folderId) && + (!query || + (asset.filename ?? asset.id) + .toLowerCase() + .includes(query.toLowerCase())) + ); + const assetOptions = [ + ...new Map( + [...assets, ...(queried ?? [])].map((asset) => [asset.id, asset]) + ).values(), + ].filter((asset) => asset.type === (kind === 'voice' ? 'audio' : 'image')); + + const content = ( +
+

{t('localOnly')}

+
+ + +
+ {error && ( +

+ {error} +

+ )} + {stale && ( +
+ {t('conflict')} + +
+ )} + {tab === 'folders' ? ( + <> +
{ + event.preventDefault(); + void search({ folderId, scope, query }); + }} + > + + + + +
+
{ + event.preventDefault(); + void apply([ + { + type: 'create_folder', + folderId: crypto.randomUUID(), + name: folderName, + parentId: folderId, + }, + ]); + }} + > + + + + +
+
+ + + +
+ {preview && ( +
+

+ {t('removalSummary', { + assets: preview.assetIds.length, + folders: preview.childFolderIds.length, + destination: preview.parentId + ? folderPath(preview.parentId) + : t('unclassified'), + })} +

+
    + {preview.assetIds.map((id) => ( +
  • + {assets.find((asset) => asset.id === id)?.filename ?? id} +
  • + ))} + {preview.childFolderIds.map((id) => ( +
  • {folderPath(id)}
  • + ))} +
+ {preview.conflicts.length > 0 && ( +

{t('nameConflicts')}

+ )} + {' '} + +
+ )} +
    + {[ + ...new Map( + visibleAssets.map((asset) => [asset.id, asset]) + ).values(), + ].map((asset) => ( +
  • + + {onLocate && ( + + )} +
  • + ))} +
+ {!visibleAssets.length && ( +

{t('noAssets')}

+ )} + {cursor && ( + + )} + + ) : ( + <> + +
{ + event.preventDefault(); + if (elementId) + void apply([ + { + type: 'update_element', + elementId, + name: elementName, + description, + }, + ]); + else + void apply([ + { + type: 'create_element', + elementId: crypto.randomUUID(), + name: elementName, + kind, + description, + versionId: crypto.randomUUID(), + assetId, + }, + ]); + }} + > + + +