@uzooai/anime-ai-cli 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,14 +21,14 @@
21
21
  | `model list` | 必填 `--space personal` 或 `--space team --team-uid UID`;可选 `--type image/video/text/audio`;以返回 `items[].label` 为模型名称,按目录能力选择参数 |
22
22
  | `canvas list` / `canvas get ID` | 列表必填 `--space personal` 或 `--space team --team-uid UID`,可带 `--project-id ID --cursor CURSOR --limit 1..100`;个人空间不能带团队/项目;生成使用已有或 CLI 创建的文档和节点 |
23
23
  | `canvas create` | 必填 `--name NAME` 及 `--space personal` 或 `--space team --team-uid UID`;团队可选 `--project-id ID`(映射 canvas_project_id);返回完整画布及 id/version,不发起生成 |
24
- | `node add` | `--doc ID --type TYPE`;仅支持普通节点 text/image/video/audio、image_upload/video_upload/audio_upload、video-repaint;可选 `--node-id ID --input node-patch.json`;默认生成 UUID 节点 ID,返回 doc_id/node_id/version/node;派生节点由真实业务流程创建 |
24
+ | `node add` | `--doc ID --type TYPE`;仅支持普通节点 text/image/video/audio、image_upload/video_upload/audio_upload、video-repaint;可选 `--node-id ID --input node-patch.json`;默认生成 UUID 节点 ID,未指定 `position` 时自动找空位,返回 doc_id/node_id/version/node;派生节点由真实业务流程创建 |
25
25
  | `node update` | `--doc ID --node-id ID --input node-patch.json`;只更新指定节点,不改 id/type,返回同上 |
26
26
  | `resource upload` | `--file FILE --doc ID --type image|video|audio --mime MIME`;普通素材节点加 `--node ID`,成功后补齐网页持久化引用,检查 canvas_bound;旧画布资产接口的图片加 `--asset-type character|scene|prop --name NAME`。返回 API 的资源引用,按实际返回字段使用 |
27
27
  | `resource bind` | `--doc ID --node ID --input upload-response.json`;复用真实上传响应,补齐普通素材节点的持久化媒体字段,不重新上传 |
28
28
  | `material folders/list/get ID` | 必填 `--space personal` 或 `--space team --team-uid UID`;list 可带 `--folder-id ID --keyword TEXT --limit 1..200 --offset N`,get 返回同空间素材 |
29
29
  | `material download ID` | 必填空间参数及--output FILE,可选--sha256 HEX;核对目录/素材归属,再请求素材专属download入口,匿名下载原格式完整url,返回output/bytes/sha256/asset_id/format/可选filename及无画布/生成/入库副作用标记。仅original,不支持自定义服务端名称或转换;拒绝不回退list/get的URL |
30
30
  | `material upload` | `--file FILE --mime MIME --folder-id ID --name NAME` 及空间参数;上传到独立素材空间,先核对实时限制和目标目录 |
31
- | `material use ID` | `--doc ID --node-id FIXED_ID` 及空间参数;可选 `--input position.json`;从库导入普通素材节点,复用真实引用,不重传 |
31
+ | `material use ID` | `--doc ID --node-id FIXED_ID` 及空间参数;可选 `--input position.json`,未指定 `position` 时自动找空位;从库导入普通素材节点,复用真实引用,不重传 |
32
32
  | `material save` | `--input save.json --operation NAME` 默认只预检;加 --yes 直接保存原节点结果到素材库。输入 doc_id/node_id/space、团队 team_uid、folder_id,可选 name/description;限同空间已完成图片/视频真实任务;持久化原请求/幂等键,不写画布 |
33
33
  | `material save-get NAME` | 查询原入库 operation,已有 asset_id 时核验原素材,返回 state/task_id/folder_id/asset_id/asset/可选 last_failure;不发保存 POST |
34
34
  | `material save-recover NAME` | 默认只查询;未知结果且没有 asset_id 时,明确恢复才加 --yes 沿用原请求与幂等键。已知 asset_id 仅查询,rejected 不自动重提;账号/服务/来源改变停止 |
@@ -16,7 +16,7 @@
16
16
  4. `canvas_bound=false` 表示文件已上传但兼容字段保存未确认。先读回节点;凭原始真实上传响应执行 `resource bind --doc DOC_ID --node NODE_ID --input upload-response.json` 仅修复原节点,不重传文件。响应须与画布/节点/媒体类型绑定,已有其他素材引用时停止。已打开网页可能持有旧快照;若出现节点或引用在后续快照反复消失,先暂停写入并协调关闭该画布页面,再核对是否为用户主动删除。用户允许恢复后才恢复缺失节点;优先复用原上传/原生成任务,不能通过重做生成弥补显示缺失。CAS 冲突不自动覆盖。
17
17
  5. 网页当前“素材库”使用独立素材空间 `/material-space`,不同于旧的 `/canvas/docs/.../assets`。使用 `material folders/list/get`,每次明确 `--space personal` 或 `--space team --team-uid UID`;分页为 limit/offset。目录来自实际列表,不猜文件夹 ID。团队没有素材时不能自动退回个人素材库。
18
18
  6. 素材库上传用 `material upload --file FILE --mime MIME --folder-id FOLDER_ID --name NAME --space team --team-uid UID`。CLI 校验同空间目录和实时上传限制,用通用直传会话上传再落素材空间。结果不明先查原目录/名称,不盲目重传;当前没有素材库分片恢复命令。
19
- 7. 从库使用 `material use ASSET_ID --doc DOC_ID --node-id FIXED_ID --space team --team-uid UID`;可选 `--input position.json`(`{"position":{"x":120,"y":130}}`)。校验素材与画布同空间,从真实响应创建普通素材节点,复用 URL/file_ref,不重新上传。固定节点 ID 便于结果未知时读回核对。当前不支持跨空间导入、素材删除/迁移/收藏或文件夹编辑,不用临时 API 调用冒充已支持命令。
19
+ 7. 从库使用 `material use ASSET_ID --doc DOC_ID --node-id FIXED_ID --space team --team-uid UID`;默认在画布空位放置,可选 `--input position.json`(`{"position":{"x":120,"y":130}}`)指定位置。校验素材与画布同空间,从真实响应创建普通素材节点,复用 URL/file_ref,不重新上传。固定节点 ID 便于结果未知时读回核对。当前不支持跨空间导入、素材删除/迁移/收藏或文件夹编辑,不用临时 API 调用冒充已支持命令。
20
20
 
21
21
  ## 整理、分组和组合输出
22
22
 
@@ -36,19 +36,16 @@ CLI 业务命令直接通过 HTTP 调用后端,不加载网页或执行前端
36
36
  ## 执行顺序
37
37
 
38
38
  1. 复用有效登录,`canvas get DOC_ID` 核对 `team_uid` 与已确认空间一致,确认有写权限。记录当前版本、已有节点、边、视口和 meta;不要清空画布。
39
- 2. 确定节点类型、标题及位置;用户只指定类型时可以用可读标题与互不重叠的位置。每个创建意图先记录固定 `--node-id` 与输入文件,再写入,便于断连后识别。
40
- 3. UTF-8 保存 `node-patch.json`,例如:
39
+ 2. 确定节点类型和标题。用户未指定坐标时省略 `position`,由 CLI 根据最新快照避开已有节点和分组;指定坐标时原样使用。每个创建意图先记录固定 `--node-id` 与输入文件,再写入,便于断连后识别。
40
+ 3. 如需自定义字段,UTF-8 保存 `node-patch.json`,例如:
41
41
 
42
42
  ```json
43
43
  {
44
- "position": {"x":120,"y":130},
45
- "width":300,
46
- "height":260,
47
44
  "data":{"title":"图片节点","prompt":""}
48
45
  }
49
46
  ```
50
47
 
51
- 4. 执行一次 `uzoo-anime --profile PROFILE node add --doc DOC_ID --type image --node-id NODE_ID --input node-patch.json`。同画布按顺序创建,不并发写入完整快照。其他普通节点换成表中的 CLI 类型;不额外拼接内部组件字段或流程派生标记。
48
+ 4. 执行一次 `uzoo-anime --profile PROFILE node add --doc DOC_ID --type image --node-id NODE_ID --input node-patch.json`,没有自定义字段可省略 `--input`。同画布按顺序创建,不并发写入完整快照。其他普通节点换成表中的 CLI 类型;不额外拼接内部组件字段或流程派生标记。
52
49
  5. `canvas get DOC_ID` 读回,验证节点 ID、API type、扩展 role、中文标题、位置、数量与版本;同时核对已有节点/边/meta 未丢失、归属未变化。不要只以命令退出 0 判定网页兼容通过。
53
50
  6. 网页可用时打开同一画布并刷新,检查组件、标题及空状态。网页可能自动保存并增加版本,后续操作始终重新读取,保留网页补齐的数据。网页暂不可用时仅标记 API 保存/读取通过,网页待验证。
54
51
 
package/CHANGELOG.md CHANGED
@@ -1,13 +1,21 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.6.2
4
+
5
+ - 新建普通节点和从素材库导入时,未指定坐标会自动避开已有节点和分组。
6
+ - 保留手动指定的坐标,不移动画布上的已有节点。
7
+
8
+ ## 0.6.1
9
+
10
+ - 更新产品介绍、安装指南和项目主页链接。
11
+
3
12
  ## 0.6.0
4
13
 
5
- - 新增可选的 `install` 命令,支持交互多选和显式指定 Agent。全局 CLI 同版本可复用,Skill 从同版本包同步;不覆盖手动安装或修改过的 Skill。
6
- - 普通 npm 安装保持原行为。Windows 的 Codex 与 WorkBuddy 桌面版已验证 Skill 发现与 CLI 调用。
7
- - 随包 Skill 指导 Agent 在首次使用时查询新版;是否更新由用户决定,CLI 不自动更新。
8
- - npm 归档仅包含运行资源和公开使用说明。
14
+ - 新增 CLI 与 Agent Skill 一键安装。
15
+ - 支持选择多个 Agent、同步同版本 Skill,并保护手动修改的 Skill。
16
+ - Agent 可提示可用更新,是否更新由用户决定。
9
17
 
10
18
  ## 0.5.0
11
19
 
12
- - 初始公开版本。支持浏览器 CLI 授权、画布与节点管理、素材、普通生成与恢复、下载、样片转正片,以及已接入的图片和视频工具。
13
- - 包含完整 Skill。该版本无 `install` 子命令,请按[安装与更新说明](docs/Agent接入与版本更新.md)手动接入 Agent。
20
+ - 支持浏览器授权、画布、素材、普通生成与下载。
21
+ - 随包提供完整 Agent Skill。
package/README.md CHANGED
@@ -1,46 +1,50 @@
1
1
  # Uzoo Anime CLI
2
2
 
3
- 通过终端或 Agent 操作 Uzoo 的画布、素材和生成任务。支持 Node.js 22 或更高版本。
3
+ Uzoo 官方命令行工具与 Agent Skills。通过终端或 Agent 管理画布和创作素材,发起图片与视频生成,并查询和下载结果。
4
4
 
5
- ## 安装 CLI 与 Skill
5
+ [Uzoo 官网](https://uzooai.com/) · [npm 软件包](https://www.npmjs.com/package/@uzooai/anime-ai-cli)
6
+
7
+ ## 安装与更新
8
+
9
+ 需要 **Node.js 22 或更高版本**。
10
+
11
+ **CLI + Agent Skill**
6
12
 
7
13
  ```sh
8
- npm install --global @uzooai/anime-ai-cli@latest
9
- uzoo-anime --version
10
- uzoo-anime --help
14
+ npx @uzooai/anime-ai-cli@latest install
11
15
  ```
12
16
 
13
- **如果版本是 0.5.0:该版本没有 `install` 子命令。** 运行 `npx @uzooai/anime-ai-cli@latest install` 时出现“未知命令”,请使用上面的 npm 命令,再按 [Agent 接入与版本更新](docs/Agent接入与版本更新.md) 手动添加 Skill。重复下载同一个版本不会增加命令。
17
+ 按提示选择要接入的 Agent。支持选择 Codex、WorkBuddy 桌面版或 Claude Code,可一次选择多个。已有同版本 CLI 时,运行 `uzoo-anime install` 即可为其他 Agent 添加 Skill。
14
18
 
15
- 从 0.6.0 起支持一键安装。在该版本进入 npm 的 `latest` 后,可运行:
19
+ **仅安装 CLI**
16
20
 
17
21
  ```sh
18
- npx @uzooai/anime-ai-cli@latest install
22
+ npm install --global @uzooai/anime-ai-cli@latest
19
23
  ```
20
24
 
21
- 按提示选择一个或多个 Agent。已有同版本 CLI 的用户可以直接运行 `uzoo-anime install` 加装 Skill。
25
+ 该命令不会修改 Agent 的技能目录。若需要手动接入,运行 `uzoo-anime skill path` 找到随包 Skill,将完整目录导入对应 Agent。
22
26
 
23
- 普通 npm 安装附带完整 Skill 文件,不会修改 Agent 的技能目录。用 `uzoo-anime skill path --format json` 找到目录后,将整个目录(含 `references`)复制到目标宿主。
27
+ 更新 CLI 和 Skill 可重跑一键安装命令,选择要同步的 Agent。更新由用户决定,CLI 不会自动安装新版。
24
28
 
25
- ## 登录与使用
29
+ ## 快速开始
26
30
 
27
31
  ```sh
28
- uzoo-anime service status
32
+ uzoo-anime --version
29
33
  uzoo-anime auth login
30
- uzoo-anime auth status
34
+ uzoo-anime canvas list --space personal
31
35
  ```
32
36
 
33
- 默认服务是 [Uzoo](https://uzooai.com)。浏览器登录和 CLI 授权由用户本人完成;不要把密码、验证码或令牌粘贴到 Agent。可用 `--profile NAME` 区分本地配置,已有有效登录可继续使用。
37
+ 在浏览器中登录 Uzoo 并授权 CLI 后即可使用。团队操作须选择对应团队。使用 Agent 时,可直接提出任务,例如:“列出我的个人画布,并帮我查看指定画布中的素材。”
34
38
 
35
- - [CLI 命令](.agents/skills/uzoo-anime-cli/references/commands.md):画布和节点、素材、生成、查询与下载。
36
- - [支持的节点与工具](.agents/skills/uzoo-anime-cli/references/node-capabilities.md):区分 CLI 能力和网页专属操作。
37
- - [生成与恢复](.agents/skills/uzoo-anime-cli/references/generation-workflow.md):先确认素材、参数和实际报价;中断时查询并恢复原 operation。
38
- - [图片工具](.agents/skills/uzoo-anime-cli/references/image-tools-workflow.md)与[视频工具](.agents/skills/uzoo-anime-cli/references/video-tools-workflow.md):按需检查 FFmpeg;CLI 不自动安装媒体工具。
39
+ ## 主要功能
39
40
 
40
- 使用 CLI 写画布前,先确认对应网页标签已关闭且离开保存完成。付费任务在用户确认本次实际报价后提交。转绘旧命令仅用于有明确授权的原结果查询、下载或排障。
41
+ - **画布与节点**:查询和管理画布、节点、分组及连线。新增普通节点或从素材库导入时,未指定坐标会自动找空位。
42
+ - **素材管理**:上传、查询、导入和下载素材。
43
+ - **图片与视频**:准备生成请求、查询任务和获取结果;支持样片转正片,以及裁剪、标注、抠图、剪辑和截帧等工具。
44
+ - **任务恢复**:查询或继续原任务,保留已知任务和结果。
41
45
 
42
- ## 更新
46
+ 付费任务会在提交前展示报价,由用户确认。运行 `uzoo-anime --help` 查看可用命令及参数。部分媒体工具需要本机 FFmpeg。
43
47
 
44
- CLI 不会后台检查或自动安装新版。使用随包 Skill 的 Agent 会在会话首次使用时尝试查询 npm 稳定版,有新版才提示,由你决定是否更新;查询失败不影响原任务。手动升级 CLI 后,也请同步手动接入的 Skill。见 [Agent 接入与版本更新](docs/Agent接入与版本更新.md)。
48
+ ## 许可证
45
49
 
46
- [更新记录](CHANGELOG.md) · [MIT 许可证](LICENSE)
50
+ CLI 源码采用 [MIT 许可证](LICENSE)。Uzoo 在线服务适用相应服务条款。
@@ -1,43 +1,41 @@
1
1
  # Agent 接入与版本更新
2
2
 
3
- ## npm 0.5.0:先安装 CLI,再接入 Skill
3
+ 需要 Node.js 22 或更高版本。CLI 和完整 Skill 由同一个 npm 包分发。
4
4
 
5
- 如果 `npx ... install` 返回“未知命令”,先运行 `uzoo-anime --version`(未安装时可用 `npx @uzooai/anime-ai-cli@latest --version`)。**0.5.0 没有一键安装入口。**
5
+ ## 一键安装
6
6
 
7
- ```powershell
7
+ ```sh
8
+ npx @uzooai/anime-ai-cli@latest install
9
+ ```
10
+
11
+ 安装器会列出检测到的本地 Agent,支持多选。也可明确指定:
12
+
13
+ ```sh
14
+ npx @uzooai/anime-ai-cli@latest install --agent codex --agent workbuddy
15
+ ```
16
+
17
+ 可选名称为 `codex`、`workbuddy`(WorkBuddy 桌面版)和 `claude-code`。重复传入 `--agent` 可安装到多个宿主。自动化脚本须明确指定目标并传入 `--yes`;用 `--dry-run` 可先查看安装计划。已有同版本 CLI 时,运行 `uzoo-anime install` 即可添加 Agent Skill。
18
+
19
+ ## 只安装 CLI 或手动接入
20
+
21
+ ```sh
8
22
  npm install --global @uzooai/anime-ai-cli@latest
9
23
  uzoo-anime --version
10
24
  uzoo-anime skill path --format json
11
25
  ```
12
26
 
13
- 将返回的 `directory` **整个复制**到宿主技能目录,保留 `SKILL.md` 和 `references/`。已有同名 Skill 先备份并比较自定义修改,备份不要留在技能扫描目录内。
27
+ 普通 npm 安装不修改 Agent 的技能目录。需要手动接入时,复制完整的 `directory`,保留 `SKILL.md` 与 `references/`:
14
28
 
15
- | 本地宿主 | 用户级目标目录 |
29
+ | 宿主 | 用户级目标目录 |
16
30
  | --- | --- |
17
31
  | Codex | `~/.agents/skills/uzoo-anime-cli` |
18
32
  | WorkBuddy 桌面版 | `~/.workbuddy/skills/uzoo-anime-cli` |
19
33
  | Claude Code | `~/.claude/skills/uzoo-anime-cli` |
20
34
 
21
- 只在当前项目使用 Codex 时,可放到该项目的 `.agents/skills/uzoo-anime-cli`。其他 Agent 按各自的技能导入方式接入。打开新会话,确认能找到 Skill 且能运行 `uzoo-anime --version`;找不到命令时检查宿主的 PATH 是否包含 npm 全局命令目录。
22
-
23
- ## 含一键安装入口的版本
24
-
25
- 仅在 `uzoo-anime install --help` 可用时使用本节。0.6.0 的安装入口已在 Windows 的 Codex、WorkBuddy 桌面版验证;Claude Code 可明确指定,尚未验收真实宿主。WorkBuddy AI 或远程 Claw 不在此范围。
26
-
27
- ```sh
28
- uzoo-anime install
29
- uzoo-anime install --agent codex --agent workbuddy
30
- uzoo-anime install --agent workbuddy --dry-run
31
- ```
32
-
33
- 默认列出检测到的 Agent,允许多选,确认后才写入。脚本须明确传入至少一个 `--agent` 并使用 `--yes`。安装器复用同版本全局 CLI,只给选中的宿主同步 Skill。发现手动安装或改过的副本会停止,给出目录,供用户先备份或只选择其他宿主。安装器不会代登录、安装 Agent 或改 PATH。
34
-
35
- 当包含该功能的版本成为 npm 的 `latest` 后,才可用 `npx @uzooai/anime-ai-cli@latest install` 首次一键接入或主动升级。为已有同版本 CLI 加装新 Agent,直接用本机 `uzoo-anime install` 即可。
35
+ 打开新的 Agent 会话,确认能发现 Skill 并运行 `uzoo-anime --version`。找不到命令时,检查宿主的 PATH 是否包含 npm 全局命令目录。登录 Uzoo 时运行 `uzoo-anime auth login`,并在浏览器中本人授权。
36
36
 
37
- ## 主动更新
37
+ ## 更新与已有配置
38
38
 
39
- - 普通 npm 用户:运行 `npm install --global @uzooai/anime-ai-cli@latest`,核对版本,再从该版本更新已复制的 Skill。
40
- - 一键安装用户:通过含一键安装功能的新版入口更新 CLI,并选择所有要同步的 Agent。
41
- - 更新不要求重新登录;可继续使用原 profile 和 operation。不要因更新重提未知任务。
39
+ 要同时更新 CLI 和 Skill,重跑一键安装并选中要同步的 Agent。安装器不会覆盖手动安装或修改过的 Skill;先备份至技能扫描目录外,再按需要更新。只更新 CLI 可运行普通 npm 安装命令。
42
40
 
43
- CLI 不会后台查询或自动更新。使用随包 Skill 的 Agent 会在每个会话首次实际使用时尝试只读查询 npm 稳定版。若有新版,会展示本机和新版版本号,由你选择继续当前版或更新,以及是否同步所选 Agent 的 Skill。查不到版本不影响使用,进行中的操作先处理完再更新。版本号也可用 `uzoo-anime --version` 查看,已发布版本可用 `npm view @uzooai/anime-ai-cli version` 查询。
41
+ Agent 首次使用时可查询 npm 稳定版;有新版时由用户决定是否更新。查询失败不影响正常使用。更新时保留原 profile 和任务记录。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzooai/anime-ai-cli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -36,7 +36,8 @@
36
36
  "CHANGELOG.md",
37
37
  "LICENSE"
38
38
  ],
39
- "description": "Uzoo Anime CLI for canvas, media references and authorized generation workflows",
39
+ "description": "Official Uzoo CLI and Agent Skills for canvases, media assets, and image and video generation.",
40
+ "homepage": "https://uzooai.com/",
40
41
  "repository": {
41
42
  "type": "git",
42
43
  "url": "git+https://github.com/UzooAI/uzooai-cli.git"
package/src/canvas.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import crypto from 'node:crypto'
2
+ import { findNodePosition } from './node-placement.js'
2
3
  import { segment } from './workflows.js'
3
4
 
4
5
  // Public CLI names map to the persisted API types and roles used by the web canvas.
@@ -10,7 +11,7 @@ function createNode(id, type) {
10
11
  apiType = 'video_repain_begain'
11
12
  data = { extra: { repaint: { role: 'entry', splitMode: 'intelligent' } } }
12
13
  }
13
- return { id, type: apiType, position: { x: 120, y: 130 }, data }
14
+ return { id, type: apiType, data }
14
15
  }
15
16
 
16
17
  const object = v => v !== null && typeof v === 'object' && !Array.isArray(v)
@@ -89,6 +90,7 @@ export async function writeNode(client, { docId, nodeId, type, patch = {}, updat
89
90
  if (!update && matches.length) throw new Error('节点 id 已存在;请读取节点确认,勿重复创建')
90
91
  const node = update ? patchNode(matches[0], patch) : candidate
91
92
  if (update && !isDerivedNode(matches[0]) && isDerivedNode(node)) throw new Error(derivedNodeMessage)
93
+ if (!update && node.position === undefined) node.position = findNodePosition(doc, node)
92
94
  const nodes = update ? doc.nodes.map(n => n.id === id ? node : n) : [...doc.nodes, node]
93
95
  const body = { version: doc.version, name: doc.name, viewport: doc.viewport, nodes, edges: doc.edges, meta: doc.meta }
94
96
  // Backend CAS protects concurrent website edits. Do not reload and overwrite on conflict.
package/src/materials.js CHANGED
@@ -131,7 +131,7 @@ export async function materialUse(client, { space, teamUid, assetId, docId, node
131
131
  expectedSpace: space,
132
132
  expectedTeamUid: teamUid,
133
133
  patch: {
134
- position: position || { x: 120, y: 130 },
134
+ position,
135
135
  data: {
136
136
  title: asset.name,
137
137
  url: asset.url,
@@ -0,0 +1,78 @@
1
+ const ORIGIN = { x: 120, y: 130 }
2
+ const COLUMNS = 4
3
+ const GAP_X = 80
4
+ const GAP_Y = 64
5
+
6
+ const positive = value => Number.isFinite(Number(value)) && Number(value) > 0 ? Number(value) : 0
7
+
8
+ // Match the web snapshot's fallback sizes. data.width/height are media pixels, not canvas sizes.
9
+ function fallbackSize({ type, data = {} }) {
10
+ if (type === 'text') return [300, 300]
11
+ if (type === 'audio' || type === 'audio_upload') return [266, 156]
12
+ if (type === 'video_repain_begain') return [590, 760]
13
+ if (data.extra?.repaint?.role === 'split-detail') return [480, 540]
14
+ if (!['image', 'image_upload', 'video', 'video_upload'].includes(type)) return [486, 300]
15
+
16
+ const asset = type.endsWith('_upload')
17
+ const ratioFields = asset
18
+ ? [data.node_output?.aspect_ratio, data.aspectRatio, data.aspect_ratio, data.extra?.aspect_ratio]
19
+ : [data.generation_config?.aspect_ratio, data.extra?.aspect_ratio, data.aspectRatio, data.aspect_ratio]
20
+ const ratioValue = [...ratioFields, data.result?.aspect_ratio].find(value => String(value ?? '').trim())
21
+ const ratioMatch = /^(\d+(?:\.\d+)?)[::](\d+(?:\.\d+)?)$/.exec(String(ratioValue ?? '').trim())
22
+ const w = positive(ratioMatch?.[1])
23
+ const h = positive(ratioMatch?.[2])
24
+ const mediaWidth = positive(data.result?.width) || positive(data.width)
25
+ const mediaHeight = positive(data.result?.height) || positive(data.height)
26
+ const ratio = w && h ? positive(w / h) : asset && mediaHeight ? positive(mediaWidth / mediaHeight) : 0
27
+ if (!ratio && type === 'image_upload') return [266, 300]
28
+ const previewRatio = type === 'image'
29
+ ? Math.min(21 / 9, Math.max(9 / 16, ratio || 16 / 9))
30
+ : ratio || 16 / 9
31
+ // The browser sizes the preview from a 272px short side, plus its border and title.
32
+ return [
33
+ positive(Math.round(272 * Math.max(1, previewRatio)) + 2) || 486,
34
+ positive(Math.round(272 * Math.max(1, 1 / previewRatio)) + 28) || 300,
35
+ ]
36
+ }
37
+
38
+ function nodeSize(node) {
39
+ const [width, height] = fallbackSize({ ...node, data: node.data ?? {} })
40
+ return [
41
+ Math.max(positive(node.width) || positive(node.w), width),
42
+ Math.max(positive(node.height) || positive(node.h), height),
43
+ ]
44
+ }
45
+
46
+ export function findNodePosition(doc, node) {
47
+ const [width, height] = nodeSize(node)
48
+ const bounds = []
49
+ const add = (position, w, h) => {
50
+ if (!position || !w || !h) return
51
+ const x = Number(position.x) || 0
52
+ const y = Number(position.y) || 0
53
+ if (Number.isFinite(x) && Number.isFinite(y)) bounds.push({ x, y, right: x + w, bottom: y + h })
54
+ }
55
+ for (const existing of doc.nodes) add(existing.position, ...nodeSize(existing))
56
+ for (const group of Array.isArray(doc.meta.canvas_groups) ? doc.meta.canvas_groups : []) {
57
+ add(group?.position, positive(group?.size?.width), positive(group?.size?.height))
58
+ }
59
+ bounds.sort((a, b) => a.y - b.y)
60
+
61
+ // Find the highest free slot in each column. Skipping to a blocker’s bottom
62
+ // avoids scanning a pixel grid when the canvas already contains large nodes.
63
+ const stride = Math.ceil(Math.max(486, width)) + GAP_X
64
+ let best
65
+ for (let column = 0; column < COLUMNS; column++) {
66
+ const x = ORIGIN.x + column * stride
67
+ let y = ORIGIN.y
68
+ for (const box of bounds) {
69
+ if (box.y >= y + height + GAP_Y) break
70
+ if (box.x >= x + width + GAP_X || box.right + GAP_X <= x || box.bottom + GAP_Y <= y) continue
71
+ y = Math.ceil(box.bottom + GAP_Y)
72
+ }
73
+ if (Number.isFinite(x) && Number.isFinite(y) && (!best || y < best.y)) best = { x, y }
74
+ if (best?.y === ORIGIN.y) break
75
+ }
76
+ if (!best) throw new Error('无法计算新节点位置;请显式指定 position')
77
+ return best
78
+ }