@uzooai/anime-ai-cli 0.6.1 → 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,5 +1,10 @@
1
1
  # 更新记录
2
2
 
3
+ ## 0.6.2
4
+
5
+ - 新建普通节点和从素材库导入时,未指定坐标会自动避开已有节点和分组。
6
+ - 保留手动指定的坐标,不移动画布上的已有节点。
7
+
3
8
  ## 0.6.1
4
9
 
5
10
  - 更新产品介绍、安装指南和项目主页链接。
package/README.md CHANGED
@@ -38,7 +38,7 @@ uzoo-anime canvas list --space personal
38
38
 
39
39
  ## 主要功能
40
40
 
41
- - **画布与节点**:查询和管理画布、节点、分组及连线。
41
+ - **画布与节点**:查询和管理画布、节点、分组及连线。新增普通节点或从素材库导入时,未指定坐标会自动找空位。
42
42
  - **素材管理**:上传、查询、导入和下载素材。
43
43
  - **图片与视频**:准备生成请求、查询任务和获取结果;支持样片转正片,以及裁剪、标注、抠图、剪辑和截帧等工具。
44
44
  - **任务恢复**:查询或继续原任务,保留已知任务和结果。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzooai/anime-ai-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
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
+ }