# 画布模式接口说明(保存节点 / 保存节点关联) 画布模式:无限画布(无固定宽高),节点无限扩展、无固定根节点,仅记录两两之间的无方向多对多关联。 通用说明: - 所有接口在 `api` 路由组下,需要登录态(`bindToken / bindExportToken / checkLogin / checkSign / checkCompany` 中间件)。 - 返回格式统一为 `{ "msg": "", "code": 0, "data": {...} }`;出错时 `code` 为错误码,`msg` 为错误信息。 - 表前缀统一为 `mp_`,节点类型为字符串小写。 --- ## 1. 保存节点:POST api/canvas/saveNode 创建或更新画布节点。**不传 `node_id` 表示创建**,**传 `node_id` 表示更新**(更新时节点类型以库中为准,不允许变更)。 ### 1.1 入参说明 | 参数 | 类型 | 必填 | 说明 | | --------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `canvas_id` | int | 是 | 画布 ID | | `node_id` | int | 否 | 节点 ID,更新时必传,创建时不传 | | `node_type` | string | 创建时是 | 节点类型,枚举:`theme`-主题、`subject`-主体、`scene`-场景、`prop`-道具、`segment`-分镜、`text`-文字、`image`-图片、`video`-视频、`audio`-音频 | | `name` | string | 否 | 节点名称 | | `content` | string | 否 | 节点内容(主要用于`text` 文字节点) | | `text_prompt` | string | 否 | 文字提示词 | | `pic_prompt` | string | 否 | 图片提示词 | | `image_url` | string | 否 | 图片 URL | | `video_url` | string | 否 | 视频 URL | | `audio_url` | string | 否 | 音频 URL | | `thumbnail_url` | string | 否 | 缩略图 URL | | `duration` | int | 否 | 时长(秒),视频/音频/分镜使用 | | `pos_x` | number | 否 | 节点在画布上的 X 坐标,默认`0` | | `pos_y` | number | 否 | 节点在画布上的 Y 坐标,默认`0` | | `z_index` | int | 否 | 节点层级,默认`0` | | `size_w` | number | 否 | 节点显示宽度(可空) | | `size_h` | number | 否 | 节点显示高度(可空) | | `source_type` | string | 否 | 关联来源,枚举:`none`-无、`product`-全局资产库、`episode`-动漫分集,默认 `none` | | `source_product_id` | int | 否 | 全局资产 ID(`mp_products.id`),`source_type=product` 时使用 | | `source_anime_id` | int | 否 | 动漫 ID(`mp_animes.id`),`source_type=episode` 时使用 | | `source_episode_id` | int | 否 | 剧集 ID(`mp_anime_episodes.id`),`source_type=episode` 时使用 | | `source_ref` | string | 否 | 来源明细引用,如剧集 JSON 列表中的 key,用于定位具体列表项 | | `ext` | object/string | 否 | 扩展信息,传对象或 JSON 字符串均可,库内以 JSON 存储 | | `segment` | object | 否 | 分镜数据子对象,仅`node_type=segment` 时生效(见 1.2) | ### 1.2 segment 子对象字段 | 参数 | 类型 | 必填 | 说明 | | ------------------ | ------------- | ---- | -------------------------------------------------------------------------------------------------------- | | `segment_number` | int | 否 | 分镜序号,默认`1` | | `name` | string | 否 | 分镜名称,缺省时取节点级`name` | | `text_prompt` | string | 否 | 文字提示词,缺省时取节点级`text_prompt` | | `pic_prompt` | string | 否 | 图片提示词,缺省时取节点级`pic_prompt` | | `image_url` | string | 否 | 图片 URL,缺省时取节点级`image_url` | | `video_url` | string | 否 | 视频 URL,缺省时取节点级`video_url` | | `audio_url` | string | 否 | 音频 URL,缺省时取节点级`audio_url` | | `duration` | int | 否 | 时长(秒) | | `status` | string | 否 | 状态,枚举:`draft`-草稿、`generating`-生成中、`completed`-已完成、`failed`-失败,默认 `draft` | | `ext` | object/string | 否 | 分镜扩展信息 JSON | > 保存分镜节点时,接口会同时写入节点表与画布分镜表(`mp_canvas_segments`);查询节点时返回的数据以分镜表字段为准。 ### 1.3 示例请求体 创建普通节点(图片): ```json { "canvas_id": 1, "node_type": "image", "name": "图片-林默立绘", "image_url": "https://example.com/linmo.png", "thumbnail_url": "https://example.com/linmo_thumb.png", "pos_x": 280, "pos_y": 380 } ``` 创建分镜节点: ```json { "canvas_id": 1, "node_type": "segment", "name": "分镜1-雨夜相遇", "pos_x": 480, "pos_y": 220, "segment": { "segment_number": 1, "text_prompt": "林默在雨夜街头偶遇苏晴", "pic_prompt": "写实。全景,雨夜街头,两人对视", "image_url": "https://example.com/seg1.png", "video_url": "", "audio_url": "", "duration": 8, "status": "completed" } } ``` 更新节点(复用动漫分集中的主体列表项): ```json { "canvas_id": 1, "node_id": 2, "name": "主体-林默", "source_type": "episode", "source_anime_id": 10, "source_episode_id": 3, "source_ref": "roles.0", "pos_x": 300, "pos_y": 100 } ``` ### 1.4 返回示例 ```json { "msg": "", "code": 0, "data": { "node_id": 21 } } ``` --- ## 2. 保存节点关联:POST api/canvas/saveRels 保存某个节点的关联列表。**整体替换语义**:先清空该节点的全部关联,再写入新关联;返回实际写入的关联条数。 ### 2.1 入参说明 | 参数 | 类型 | 必填 | 说明 | | --------------- | ----- | ---- | -------------------------------------------------------------------------------------------------------- | | `canvas_id` | int | 是 | 画布 ID | | `node_id` | int | 是 | 中心节点 ID | | `related_ids` | array | 是 | 要关联的节点 ID 数组;也可传逗号分隔的字符串(如`"3,7,9"`)。传空数组/空字符串表示清空该节点的所有关联 | ### 2.2 规则说明 - 关联**无方向**:`A 关联 B` 与 `B 关联 A` 等价,内部统一按(小 id,大 id)存储,同一对节点只保留一条。 - 支持**多对多**:一个节点可以关联任意多个节点,1:1、1:N、N:1、N:N 均由该表表达。 - 自动过滤:与自身关联、跨画布的节点、不存在的节点会被忽略,不影响其他关联写入。 - `related_ids` 会整体替换该节点原有全部关联,不需要前端先查再删。 ### 2.3 示例请求体 ```json { "canvas_id": 1, "node_id": 2, "related_ids": [6, 8, 12, 16] } ``` 清空节点 2 的全部关联: ```json { "canvas_id": 1, "node_id": 2, "related_ids": [] } ``` ### 2.4 返回示例 ```json { "msg": "", "code": 0, "data": { "success": 1, "rel_count": 4 } } ``` --- ## 3. 关联查询接口(配套) ### 3.1 获取节点关联:GET api/canvas/nodeRels 入参: | 参数 | 类型 | 必填 | 说明 | | ------------- | ---- | ---- | ------- | | `canvas_id` | int | 是 | 画布 ID | | `node_id` | int | 是 | 节点 ID | 返回:`rel_ids`(关联节点 ID 数组)+ `related_nodes`(关联节点完整信息数组)。 ### 3.2 画布关系图:GET api/canvas/graph 入参: | 参数 | 类型 | 必填 | 说明 | | ------------- | ---- | ---- | ------------------------------------------------------------------------- | | `canvas_id` | int | 是 | 画布 ID | | `node_id` | int | 否 | 中心节点 ID;不传返回画布全部节点与关联,传了返回以该节点为中心的局部子图 | | `depth` | int | 否 | 局部子图深度(1-5),默认`1`,仅 `node_id` 传入时生效 | 返回:`nodes`(节点数组)+ `rels`(关联数组,每项含 `node_id_a` / `node_id_b`),局部子图模式额外返回 `center_node_id`、`depth`、`levels`。 局部子图模式下(传了 `node_id`),节点与关联都附带 `level` 字段,并提供 `levels` 层级摘要,方便前端逐层加载与动画: - `nodes[].level`:节点距中心节点的跳数,中心节点为 `0`,直接邻居为 `1`,以此类推; - `rels[].level`:关联首次被 BFS 触及的轮次(`depth=1` 时全部为 `1`,即中心节点的直接关联); - `levels`:数组,每项 `{ level, node_ids, rel_ids }`,`node_ids` / `rel_ids` 分别为该层包含的节点 ID 与关联 ID 列表(`level=0` 为中心节点)。 前端渲染建议:直接用平铺的 `nodes` / `rels` 画节点和连线;需要"逐层展开/动画"时,按 `level` 过滤即可(如 `nodes.filter(n => n.level <= 1)`),或直接遍历 `levels` 摘要。 > 局部子图的 `rels` 只包含从中心节点展开过程中触及的关联:`depth=1` 时仅返回中心节点的直接关联,不会返回"邻居之间的边"(例如节点 9 的邻居 5 与 18 之间若存在关联,不会在 `depth=1` 的结果中出现,需 `depth=2` 才会包含)。 > 以演示数据中心节点 9、`depth=2` 为例的返回结果图示见 [canvas_subgraph_depth2.png](images/canvas_subgraph_depth2.png):返回 9 个节点、9 条边;节点 1 与 2 之间的边虽然两者都在子图内,但 BFS 展开过程中未触及,因此不返回。 ### 3.3 节点详情:GET api/canvas/nodeInfo 入参:`canvas_id`、`node_id`。返回节点完整信息(分镜节点含分镜数据)+ `rel_ids` + `related_nodes`。 --- ## 4. 接口测试演示数据 仓库内已提供演示数据生成器 `database/seeders/CanvasDemoSeeder.php`,会生成一个名为「接口测试画布-示例」的画布,包含: - 20 个节点:主题×2、主体×2、场景×2、道具×2、分镜×3、文字×2、图片×3、视频×2、音频×2 - 27 条无方向关联,节点间构成连通网络 - 3 条画布分镜数据(含提示词、图片/视频 URL、状态) 运行前确认画布数据表已创建(`php artisan migrate`),然后执行: ```powershell # 指定测试用户的 uid / cpid(不指定默认 uid=1, cpid=1) $env:CANVAS_DEMO_UID = 123 $env:CANVAS_DEMO_CPID = 456 php artisan db:seed --class=CanvasDemoSeeder ``` 重复执行会先清理同名演示画布再重新生成。执行成功后控制台会输出 `canvas_id` 与 20 个节点的编号→ID 映射,便于直接拼接测试请求。 测试建议顺序: 1. `GET api/canvas/list` 查看画布; 2. `GET api/canvas/graph?canvas_id=1` 查看全部节点与关联; 3. `GET api/canvas/nodeRels?canvas_id=1&node_id=8` 查看分镜节点的关联; 4. `POST api/canvas/saveRels` 修改某节点关联后,再调用 `graph` 或 `nodeRels` 验证; 5. `POST api/canvas/saveNode` 新建/更新节点后,用 `nodeInfo` 验证。