@ninkch/dsh-infinite-canvas 0.17.16 → 0.17.18
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.
- package/assets/canvas-craft/SKILL.md +43 -0
- package/assets/canvas-layout/SKILL.md +61 -0
- package/assets/canvas-studio/SKILL.md +27 -16
- package/client.js +340 -65
- package/index.js +14 -14
- package/package.json +1 -1
|
@@ -31,6 +31,49 @@ B:……
|
|
|
31
31
|
- **一致性**:人名、地名、道具名与设定集**逐字一致**,禁止擅自改名或新增设定。
|
|
32
32
|
- **输出格式**:直接给可用剧本正文,不要解释、不要提纲、不要"待生成"占位。
|
|
33
33
|
|
|
34
|
+
|
|
35
|
+
## 二·五、资产设定图版式(角色/道具/场景的专业排版,拒绝简单三视图)
|
|
36
|
+
|
|
37
|
+
简单三视图信息量不足——设定图要做成**专业设定集排版**:文字信息 + 主图 + 特写 + 表情/色卡
|
|
38
|
+
分区呈现,一图读懂资产的全部视觉设定。所有面板文字一律**简体中文、清晰可读**;
|
|
39
|
+
同一资产在所有分格中外观**完全一致**(发型/服装/配色逐格复用设定集关键词)。
|
|
40
|
+
|
|
41
|
+
### 角色设定图(16:9 横版——主图与三视图占大版面)
|
|
42
|
+
- **版面主体(约三分之二)给画面**:中央大幅全身立绘主图(正面站姿),主图下方一排
|
|
43
|
+
**三视图(正面 / 侧面 / 背面 全身)**——三视图是必须项,后续视频要靠它保证侧背面的准确形态。
|
|
44
|
+
- **右侧窄栏做辅助信息(压缩排布)**:
|
|
45
|
+
- 信息条:姓名 / 性别 / 年龄 / 身份,3–4 行小字;
|
|
46
|
+
- 脸部特征:一格正面特写(或正/侧两小格)+ 一两行五官标注;
|
|
47
|
+
- 服饰特征:2–3 行服装要点;
|
|
48
|
+
- 表情条:一行 4–6 个小头像,逐个标注表情名(按角色性格取舍,如 温柔 / 惊讶 / 坚定)。
|
|
49
|
+
- **细节格(可选)**:2–4 个小格——发饰 / 佩饰 / 纹样特写,各配一行小字。
|
|
50
|
+
- 所有分格中同一角色外观完全一致(发型 / 服装 / 配色逐格复用设定集关键词);
|
|
51
|
+
面板文字一律简体中文清晰可读。
|
|
52
|
+
- 提示词结构示例:「角色人设图(16:9 横版设定集排版):中央大幅全身立绘,主图下方三视图一排
|
|
53
|
+
(正面/侧面/背面全身);右侧窄栏:姓名性别年龄小字信息、一格脸部特写、一行表情小头像;
|
|
54
|
+
面板文字全部写进提示词,模型才能排出完整版式。」
|
|
55
|
+
|
|
56
|
+
### 道具设定图(横竖皆可——按道具特性选,不限制)
|
|
57
|
+
- **画幅选择**:长条形/单件道具(剑、箫、杖)竖版更能展示完整形态;
|
|
58
|
+
组合道具、带场景感或横向结构的道具用横版能并列更多细节与色卡——排版逻辑不变,画幅自由。
|
|
59
|
+
- **顶部**:道具名大标题 + 一行类别副标题(如「火属性上古仙兵」)。
|
|
60
|
+
- **中央主图**:道具斜 45° 悬浮展示,带氛围光/粒子效果,占画面主体。
|
|
61
|
+
- **细节特写 2–3 格**:材质纹理、关键部件(如「箫身细节:赤玉雕金」),各配一行小字。
|
|
62
|
+
- **侧栏侧视图**:同一道具的正侧面完整形态,标题「侧视图」。
|
|
63
|
+
- **底部颜色参考**:3–5 个圆形色卡,每个下方标注颜色名(如「赤红 / 熔金 / 绛紫」)。
|
|
64
|
+
- 点缀一两句道具铭文/短诗式文案(如「一簫起,万火归墟」)。
|
|
65
|
+
|
|
66
|
+
### 场景设定图(主场景图 + 环绕补充小图)
|
|
67
|
+
- **主场景图占画面主体**(全景建立镜头,交代空间结构与光线方向)——不要切成几张等大的长条格,
|
|
68
|
+
那样每格都是局部长图,场景信息反而看不全。
|
|
69
|
+
- **环绕补充小图(3–6 张,位置与数量不拘)**,参考版式:
|
|
70
|
+
- 右侧一列:**多角度图**(俯瞰、贴地、第一人称视线等其他机位)+ **其他时间图**(夜景 / 雨雾 /
|
|
71
|
+
逆光等不同时间段或天气下的同一场景);
|
|
72
|
+
- 底部一排:**细节格**(标志物 / 入口 / 材质纹理 / 关键道具在场景中的位置)。
|
|
73
|
+
- 每张小图下方一行标注(机位 / 时间段 / 内容,如「入口 · 夜」「雨中 · 逆光」)。
|
|
74
|
+
- 上述布局是大致参考、不强制;所有格子的光线方向、天气、色调须与主图统一
|
|
75
|
+
(展示不同时间段时在标注中说明)。
|
|
76
|
+
|
|
34
77
|
### 画面感要点
|
|
35
78
|
- 用一个**具体的可视细节**替代形容词:不写"他很悲伤",写"他把没送出的信折了三次,塞回口袋"。
|
|
36
79
|
- 每场至少一个"能被记住的画面"(记忆点)。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: canvas-layout
|
|
3
|
+
description: 画布节点布局纪律:一类型一列、竖向排列、绝不重叠、批量建点后一键重排。在建任何节点之前加载本技能;节点一多,布局就是可操作性本身。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 画布布局纪律(一类型一列 · 绝不重叠)
|
|
7
|
+
|
|
8
|
+
节点一多,画布就难操作(画布不能无限缩小)。**布局不是审美问题,是可操作性问题**——
|
|
9
|
+
200 个节点如果类型交叉、互相重叠,就只能一块一块挪,无法整体把握。
|
|
10
|
+
本技能是「建节点」之前的硬约束,与 `canvas-studio`(操作流程)、`canvas-craft`(创作方法论)配合使用。
|
|
11
|
+
|
|
12
|
+
## 一、铁律
|
|
13
|
+
|
|
14
|
+
1. **一类型一列**:同类型节点永远排在**同一竖列**里、从上往下顺次排列。
|
|
15
|
+
列的固定顺序(从左到右):
|
|
16
|
+
**剧本 → 场景文本 → 分镜表 → 资产图(角色/场景/道具)→ 视频 → 成片**。
|
|
17
|
+
绝不把同类型节点横向散开,也绝不与其他类型混列。
|
|
18
|
+
2. **连线不改列**:连线只表达数据流(剧本→分镜、分镜→视频、资产→参考图)。
|
|
19
|
+
分镜表不会因为连了"某场文本"就移动到那一列旁边——它永远待在「分镜表」列里。
|
|
20
|
+
3. **绝不重叠**:任意两个节点框不得相交,最小间距 24px。
|
|
21
|
+
插件已内建自动挤开(节点变高、被拖动、被拉大时,相交的节点会被自动推到其下方),
|
|
22
|
+
**不要手动去挪别人的节点**——你只要保证"建点顺序 = 列内顺序"即可。
|
|
23
|
+
4. **建点顺序就是叙事顺序**:同类型节点按 场次/序号 顺序创建,
|
|
24
|
+
后建的自动追加在列尾,列的竖向顺序天然等于叙事顺序。
|
|
25
|
+
**不要回填到列中间**(会造成列内乱序,后续只能靠 `canvas_relayout` 修正)。
|
|
26
|
+
5. **命名带序**:同类节点名字里带序号(`第1集-场1文本` / `第1集-场1分镜表` / `角色-林晚 设定图`),
|
|
27
|
+
便于在长列里定位。
|
|
28
|
+
6. **类型多时子列**:资产图较多(角色/场景/道具混在一列会很长)时,可按
|
|
29
|
+
「角色图列 / 场景图列 / 道具图列」再分列——但每个子列内部仍遵守竖向排列。
|
|
30
|
+
|
|
31
|
+
## 二、标准建点节奏(保持列干净的写法)
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
1) 剧本节点 (1 个,列 1)
|
|
35
|
+
2) 场景文本 × N 按场次顺序建(列 2,竖向 = 场1…场N)
|
|
36
|
+
3) 分镜表 × N 按场次顺序建(列 3;用 connectFrom 连到对应场景文本,但列归属不变)
|
|
37
|
+
4) 资产图 × M 角色 → 场景 → 道具 顺序建(列 4)
|
|
38
|
+
5) 视频节点 × N 按场次顺序建(列 5)
|
|
39
|
+
6) 成片节点 (列 6,通常 1 个)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- 每建完一批同类节点,列尾自然延伸,**不要中途跳去建另一类型的节点再回来**。
|
|
43
|
+
- 需要给已建节点补连线时,用 `canvas_connect` ——**连线不会移动节点**,放心连。
|
|
44
|
+
|
|
45
|
+
## 三、布局乱了怎么办(不要手工挪)
|
|
46
|
+
|
|
47
|
+
- **一键重排**:`canvas_relayout({ projectId })` —— 按行重排:
|
|
48
|
+
横向数据流决定列(文本→分镜表→视频),竖向关系(承接 `prev` / 首帧 `frame`)留在同列,
|
|
49
|
+
同行 = 一个横向连通组(「场N文本 ↔ 场N分镜表」对齐),行高取最高节点、行间保留间距、绝不重叠。
|
|
50
|
+
- 重排是**幂等**的:可以反复调用,不会破坏连线与内容。
|
|
51
|
+
- 批量创建(一次建 5 个以上节点)之后,**紧接着调用一次 `canvas_relayout`**,
|
|
52
|
+
把新节点规整到列里,再继续后续步骤。
|
|
53
|
+
|
|
54
|
+
## 四、自检(每次建完一批节点后)
|
|
55
|
+
|
|
56
|
+
- [ ] 同类型节点是否全在同一竖列?竖向顺序是否等于叙事顺序?
|
|
57
|
+
- [ ] 有没有任意两个节点框相交(含 24px 间距)?
|
|
58
|
+
- [ ] 有没有类型交叉(例如某个分镜表跑到资产图列里)?
|
|
59
|
+
- [ ] 列数是否等于当前用到的类型数(而非更多)?
|
|
60
|
+
|
|
61
|
+
任何一项不满足 → 直接 `canvas_relayout({ projectId })`,不要手工一个个挪。
|
|
@@ -36,7 +36,7 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
|
|
|
36
36
|
(`canvas_delete_node` / `canvas_delete_project`)。
|
|
37
37
|
7. **完成后让用户看到结果**:调用 `canvas_open({ projectId })`,画布会在数秒内自动跳转。
|
|
38
38
|
8. **一致性告警必须闭环。** 分镜节点生成/更新后,`canvas_node_status` 会返回 `warnings`——
|
|
39
|
-
分镜中出现但**未在设定集登记**或**已登记但描述为空**的实体清单(含画面/动作文本里的
|
|
39
|
+
分镜中出现但**未在设定集登记**或**已登记但描述为空**的实体清单(含画面/动作文本里的 `@资产名@` 提及)。
|
|
40
40
|
只要 `warnings` 非空:**逐条** `canvas_update_bible` 补全(名称与告警**逐字一致**、描述写实可生成),
|
|
41
41
|
再生成参考图;**绝不忽略、绝不跳过、绝不只补一部分**。设定集更新后告警会自动重算
|
|
42
42
|
(几秒内随文档同步刷新),以最新一次 `canvas_node_status` 为准。
|
|
@@ -109,11 +109,11 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
|
|
|
109
109
|
- `canvas_create_node({ projectId, type: 'video-gen', name: '第X节-场Y视频', connectFrom: 分镜表节点id, duration: 各镜时长之和 })`
|
|
110
110
|
(不传 connectFrom 且项目里有已拆解镜头的分镜表时会自动连线;prompt 留空);
|
|
111
111
|
- **【禁止逐镜建节点】**一段 10–30 秒的场景视频**不需要逐镜的图片节点或"镜头图→镜头视频"链路**:
|
|
112
|
-
|
|
112
|
+
多镜头一致性靠角色/场景/道具**参考图资产**(`@资产名@` 引用自动挂载)保证,视频模型在一次调用里完成全部镜头。
|
|
113
113
|
最多为**开场首帧或 1–2 个关键镜头**补定帧图(接同一视频节点的 frame/ref 端口)。
|
|
114
114
|
一旦发现自己在批量创建"第X节-场Y-镜Z"这类镜头图片/视频节点 = 走错路,**立即停止**并回到本条。
|
|
115
115
|
- 参考图:把角色/场景/道具参考图资产连到视频生成节点的 `ref` 端口(`canvas_connect`);
|
|
116
|
-
提示词里的
|
|
116
|
+
提示词里的 `@资产名@` 命中的资产也会自动挂上
|
|
117
117
|
- **【跨段连贯:分镜承接 + 尾帧 → 首帧】**连贯分两层,先文本后画面:
|
|
118
118
|
**①分镜承接(文本层,先做)**:上一分镜表 `tail` → 本分镜表 `prev`(见硬约束 13 / 工作流第 3 步),
|
|
119
119
|
让分镜第 1 镜从上一场尾镜的最后一个动作继续、重叠 ≤0.2s、背景声连续;
|
|
@@ -132,14 +132,22 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
|
|
|
132
132
|
要让同一角色**跨视频保持音色**:用视频/成片节点的「提取声音参考」按钮——
|
|
133
133
|
**输入角色名即可**:系统自动按分镜 `dialogue` 台词标注定位该角色的全部时间窗,
|
|
134
134
|
逐窗取音轨**拼接为一个声音参考文件**(`角色-<名> 声音`,自动绑定实体),
|
|
135
|
-
之后该角色的视频提示词里
|
|
135
|
+
之后该角色的视频提示词里 `@<名称>@`(双 @ 包裹)会自动把这条声音参考放进 `audios[]`,
|
|
136
136
|
模型生成的对白将向参考音色对齐(携带声音参考时会自动强制开启同步音频)。
|
|
137
137
|
时间窗归属靠分镜的 `dialogue` 标注(对白逐字来自剧本、按镜头分配,一镜尽量单说话人);
|
|
138
138
|
分镜缺少台词标注时按钮会降级为手动模式并提示重新生成分镜。
|
|
139
139
|
- 触发:`canvas_run_node` → 轮询;整段视频一次生成完成
|
|
140
|
-
5.
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
5. **生成角色/场景/道具参考图**:为设定集里的每个条目创建 image-gen 节点
|
|
141
|
+
(命名规范「角色-<名> 设定图」/「场景-<名> 设定图」/「道具-<名> 设定图」),
|
|
142
|
+
**prompt 必须按 `canvas-craft`「资产设定图版式」一节的排版模板写详细版式提示词,
|
|
143
|
+
禁止只写一句三视图/外观关键词**——信息量不足,撑不起后续视频的角色一致性。要点(完整模板见 canvas-craft):
|
|
144
|
+
- **角色设定图**(16:9 横版):**主图 + 三视图一排(正/侧/背全身,必须项)占大版面(约 2/3)**;
|
|
145
|
+
右窄栏压缩辅助信息(信息条 3–4 行小字 / 脸部特写一格 / 服饰要点 2–3 行 / 表情条一行 4–6 个小头像);
|
|
146
|
+
细节格可选;所有分格中同一角色外观完全一致
|
|
147
|
+
- **道具设定图**(横竖皆可,按道具特性选):顶部道具名大标题+类别副标题;中央主图(斜 45° 悬浮+氛围光);
|
|
148
|
+
材质/部件细节特写带名称小字;侧栏侧视图;底部圆形色卡 3–5 枚带颜色名
|
|
149
|
+
- **场景设定图**(主图+环绕小图):一张主场景大图占主体,周边环绕补充小图(多角度 / 其他时间段 / 细节格,3–6 张各配标注)——大致参考不强约束
|
|
150
|
+
- 面板文字一律简体中文且清晰可读;设定集外观关键词逐格复用,保证跨格一致
|
|
143
151
|
- 需要补连任意两节点时用 `canvas_connect({ projectId, fromNode, toNode })`(端口类型必须匹配;
|
|
144
152
|
连分镜表时系统按语义自动选口:剧本/文本 → `script`,分镜表 → `prev`,不用手写端口)
|
|
145
153
|
- **布局乱了/节点重叠**:`canvas_relayout({ projectId })` 一键重排——横向数据流决定列
|
|
@@ -150,38 +158,41 @@ description: 用无限画布(Infinite Canvas)做短剧/视频创作时使用
|
|
|
150
158
|
|
|
151
159
|
## 三、@引用与资产命名(跨集一致性的核心手法)
|
|
152
160
|
|
|
153
|
-
**写图片/视频提示词时必须用
|
|
161
|
+
**写图片/视频提示词时必须用 `@资产名@` 引用设定集条目(前后各一个 @ 精确包裹,缺一不可)**,
|
|
162
|
+
不要只写名字,更不要写单个 @:
|
|
154
163
|
|
|
155
164
|
```
|
|
156
|
-
|
|
165
|
+
中近景,@张山@ 站在 @褶皱巷@ 的雨里,手里攥着 @火漆信@,暖黄门灯,写实电影感
|
|
157
166
|
```
|
|
158
167
|
|
|
159
168
|
插件会自动完成(用户也能在节点里看到"@引用命中"):
|
|
160
169
|
|
|
161
|
-
-
|
|
170
|
+
- `@张山@` → 取设定集里「张山」的外观描述,并把它名下**所有参考图资产**按顺序追加为 `[图N]`
|
|
162
171
|
- 连线参考图排在最前(`[图1..n]`,顺序可在节点里上下调整),`@` 命中的资产按**实体出现顺序**接在后面
|
|
163
|
-
-
|
|
164
|
-
|
|
172
|
+
- `@资产名@` 在输入框里会**蓝色显示、两个 @ 自动隐藏**(人类可读),送给模型前会自动转成带 `[图N]`
|
|
173
|
+
与设定描述的标准提示词
|
|
174
|
+
- **句末引用也必须闭合**:正确「……举起 @火漆信@。」;错误「……举起 @火漆信。」——漏写闭合 @ 的引用
|
|
175
|
+
不会生效(生成时会按设定集名称自动补全修复,但不要依赖)
|
|
176
|
+
- 未命中设定集的名字不会报错,但会失去一致性保障——所以先补设定集再加 `@资产名@`
|
|
165
177
|
|
|
166
178
|
**资产命名规范(必须遵守,否则跨集引用会断)**:
|
|
167
179
|
|
|
168
180
|
| 用途 | 命名 | 例子 |
|
|
169
181
|
| --- | --- | --- |
|
|
170
|
-
|
|
|
182
|
+
| 角色参考图/设定图 | `角色-<名称> <说明>` | `角色-张山 设定图`、`角色-张山 参考图` |
|
|
171
183
|
| 场景参考图 | `场景-<名称>` | `场景-褶皱巷` |
|
|
172
184
|
| 道具参考图 | `道具-<名称>` | `道具-火漆信` |
|
|
173
185
|
| 分镜表整段视频 | `<集数>-<场次> 多镜头视频` | `第1集-场3 多镜头视频` |
|
|
174
186
|
|
|
175
187
|
生成角色/场景/道具参考图时,**用 `canvas_create_node` 的 `name` 直接写这个规范名**;
|
|
176
|
-
插件会把资产记录绑定到对应实体(`entity` 字段),之后任何
|
|
188
|
+
插件会把资产记录绑定到对应实体(`entity` 字段),之后任何 `@资产名@` 都能精确取到它。
|
|
177
189
|
|
|
178
190
|
**每集/后续工作的一致性纪律**:
|
|
179
191
|
|
|
180
192
|
1. 每集开工前先 `canvas_get_bible`,**只沿用已有名称**;新元素先 `canvas_update_bible` 补卡再使用。
|
|
181
193
|
2. **绝不改名**:一个角色的名字一旦写进设定集,后续所有集数、所有提示词都用同一个名字。
|
|
182
|
-
3.
|
|
194
|
+
3. 已生成的参考图资产**持续复用**:`@资产名@` 会自动带上它们,不需要重复生成。
|
|
183
195
|
4. 若某实体还没有参考图,先为它建一个 `image-gen` 节点生成(用规范命名),再进入镜头生成。
|
|
184
|
-
|
|
185
196
|
## 四、节点类型与字段
|
|
186
197
|
|
|
187
198
|
| 类型 | 用途 | 主要字段 |
|
package/client.js
CHANGED
|
@@ -504,6 +504,9 @@ window.__ModuleLoader__.load({
|
|
|
504
504
|
'bitrate': '码率',
|
|
505
505
|
'bitrateAuto': '自动(浏览器默认)',
|
|
506
506
|
'runRequestFailed': '对话触发的生成未能执行',
|
|
507
|
+
'stoppedNote': '已手动停止',
|
|
508
|
+
'stoppedTaskKept': '视频任务 id 已保留,可凭 id 回查结果',
|
|
509
|
+
'stoppedToast': '已手动停止生成',
|
|
507
510
|
'apiTimeout': '请求超时(毫秒)',
|
|
508
511
|
'uploadFailed': '导入失败',
|
|
509
512
|
'syncFailed': '与 Host 同步失败',
|
|
@@ -908,6 +911,9 @@ window.__ModuleLoader__.load({
|
|
|
908
911
|
'bitrate': 'Bitrate',
|
|
909
912
|
'bitrateAuto': 'Auto (browser default)',
|
|
910
913
|
'runRequestFailed': 'Chat-triggered generation failed',
|
|
914
|
+
'stoppedNote': 'Manually stopped',
|
|
915
|
+
'stoppedTaskKept': 'video task id kept for lookup',
|
|
916
|
+
'stoppedToast': 'Generation stopped',
|
|
911
917
|
'apiTimeout': 'Request timeout (ms)',
|
|
912
918
|
'uploadFailed': 'Import failed',
|
|
913
919
|
'syncFailed': 'Failed to sync with Host',
|
|
@@ -1027,10 +1033,11 @@ window.__ModuleLoader__.load({
|
|
|
1027
1033
|
.dshic-rs-n{top:-3px;left:14px;right:14px;height:7px;cursor:ns-resize;}
|
|
1028
1034
|
.dshic-rs-se{right:-4px;bottom:-4px;width:16px;height:16px;cursor:nwse-resize;}
|
|
1029
1035
|
.dshic-node-body{padding:8px;display:flex;flex-direction:column;gap:6px;font-size:12px;flex:1;min-height:0;overflow:auto;}
|
|
1030
|
-
/*
|
|
1036
|
+
/* 手动拉过高高度的节点:主体内的文本框随节点一起拉伸(拖高=对话框变高,而不是只撑外框);
|
|
1037
|
+
文本框不允许被 flex 压扁(分镜列表/流式变高时宁可下方截断,输入框保持自身可读高度) */
|
|
1031
1038
|
.dshic-node.h-manual .dshic-node-body{overflow:hidden;}
|
|
1032
1039
|
.dshic-node.h-manual .dshic-node-body > .dshic-textarea{flex:1 1 auto;min-height:0;}
|
|
1033
|
-
.dshic-node.h-manual .dshic-node-body > .dshic-prompt{flex:1 1 auto;display:flex;flex-direction:column;}
|
|
1040
|
+
.dshic-node.h-manual .dshic-node-body > .dshic-prompt{flex:1 1 auto;display:flex;flex-direction:column;flex-shrink:0;}
|
|
1034
1041
|
.dshic-node.h-manual .dshic-node-body > .dshic-prompt > .dshic-prompt-input{flex:1 1 auto;min-height:0;}
|
|
1035
1042
|
.dshic-textarea{width:100%;box-sizing:border-box;min-height:56px;resize:vertical;border:1px solid var(--dsw-alias-border-l2);
|
|
1036
1043
|
border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;
|
|
@@ -1174,16 +1181,21 @@ window.__ModuleLoader__.load({
|
|
|
1174
1181
|
display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;}
|
|
1175
1182
|
.dshic-textcard-sub{font-size:10px;line-height:14px;color:var(--dsw-alias-label-secondary);}
|
|
1176
1183
|
/* @引用高亮输入框:字号/行高/内距与 .dshic-textarea 完全一致(背景层与透明输入框必须逐像素对齐) */
|
|
1177
|
-
|
|
1184
|
+
/* overflow+圆角:背层是绝对定位,父级高度由 textarea 撑开时 bottom/inset 不一定能把背层锁死,
|
|
1185
|
+
多出来的行会画出边框。裁切必须做在由输入框撑高的这一层上,框内保留、框外遮住(含圆角)。 */
|
|
1186
|
+
.dshic-prompt{position:relative;display:block;width:100%;overflow:hidden;border-radius:8px;}
|
|
1178
1187
|
.dshic-prompt-back,.dshic-prompt-input{font:inherit;font-size:12px;line-height:18px;padding:7px 9px;box-sizing:border-box;
|
|
1179
1188
|
white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;}
|
|
1180
|
-
.dshic-prompt-back{position:absolute;
|
|
1181
|
-
border:1px solid transparent;border-radius:
|
|
1189
|
+
.dshic-prompt-back{position:absolute;top:0;left:0;right:0;height:100%;overflow:hidden;pointer-events:none;color:var(--dsw-alias-label-primary);
|
|
1190
|
+
border:1px solid transparent;border-radius:8px;}
|
|
1182
1191
|
.dshic-prompt-ph{color:var(--dsw-alias-label-tertiary);}
|
|
1183
1192
|
.dshic-prompt-input{position:relative;background:transparent;color:transparent;caret-color:var(--dsw-alias-label-primary);
|
|
1184
|
-
resize:vertical;}
|
|
1193
|
+
resize:vertical;overflow-y:auto;scrollbar-width:none;}
|
|
1194
|
+
/* 隐藏输入框滚动条:滚动条占位会让透明输入框与背层的换行点不一致(错位的另一来源);滚轮滚动不受影响 */
|
|
1195
|
+
.dshic-prompt-input::-webkit-scrollbar{width:0;height:0;}
|
|
1185
1196
|
.dshic-prompt-input::placeholder{color:transparent;}
|
|
1186
|
-
.dshic-mention{color:#2f7cf6;
|
|
1197
|
+
.dshic-mention{color:#2f7cf6;}
|
|
1198
|
+
/* 注意:@名称 高亮不能加粗——加粗字形更宽,背层与透明输入框宽度不一致会导致逐字错位 */
|
|
1187
1199
|
/* 生成中的一行思考内容:单行、省略号,让用户看到"在动"而不是卡住 */
|
|
1188
1200
|
.dshic-stream{display:flex;align-items:center;gap:5px;font-size:10px;line-height:14px;color:var(--dsw-alias-label-secondary);
|
|
1189
1201
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 2px;font-variant-numeric:tabular-nums;}
|
|
@@ -1256,6 +1268,29 @@ window.__ModuleLoader__.load({
|
|
|
1256
1268
|
.dshic-zoom-val{min-width:52px;text-align:center;font-size:12px;color:var(--dsw-alias-label-secondary);
|
|
1257
1269
|
font-variant-numeric:tabular-nums;cursor:pointer;background:transparent;border:none;height:26px;font:inherit;font-size:12px;}
|
|
1258
1270
|
.dshic-zoom-val:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent);color:var(--dsw-alias-label-primary);}
|
|
1271
|
+
|
|
1272
|
+
/* ── 视觉精修层(统一质感;追加在基础规则之后生效)──────────────── */
|
|
1273
|
+
.dshic-node{border-radius:14px;box-shadow:0 1px 2px rgba(0,0,0,.10),0 6px 18px rgba(0,0,0,.10);}
|
|
1274
|
+
.dshic-node:hover{box-shadow:0 2px 4px rgba(0,0,0,.10),0 10px 26px rgba(0,0,0,.14);}
|
|
1275
|
+
.dshic-node.is-selected{box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 38%,transparent),0 10px 26px rgba(0,0,0,.14);}
|
|
1276
|
+
.dshic-node.st-running{box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent),0 8px 22px rgba(0,0,0,.12);}
|
|
1277
|
+
.dshic-node-head{height:32px;padding:0 10px;}
|
|
1278
|
+
.dshic-node-head .dshic-node-label,.dshic-node-head strong{font-weight:600;letter-spacing:.2px;}
|
|
1279
|
+
.dshic-node-body{padding:10px;display:flex;flex-direction:column;gap:8px;}
|
|
1280
|
+
.dshic-gen{height:32px;font-weight:600;letter-spacing:.3px;box-shadow:0 1px 3px rgba(0,0,0,.18);}
|
|
1281
|
+
.dshic-gen:hover:not(:disabled){box-shadow:0 2px 6px rgba(0,0,0,.22);}
|
|
1282
|
+
.dshic-textarea:focus,.dshic-input:focus,.dshic-prompt-input:focus{border-color:var(--dsw-alias-brand-primary);
|
|
1283
|
+
box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);}
|
|
1284
|
+
/* 高亮框自己 overflow:hidden,子元素的光圈会被裁掉;光圈画在外层,外层自己的阴影不受自身 overflow 影响 */
|
|
1285
|
+
.dshic-prompt:focus-within{box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);}
|
|
1286
|
+
.dshic-prompt:focus-within .dshic-prompt-input{box-shadow:none;}
|
|
1287
|
+
.dshic-shot-label{font-size:10px;letter-spacing:.4px;color:var(--dsw-alias-label-secondary);}
|
|
1288
|
+
.dshic-hint{line-height:15px;}
|
|
1289
|
+
.dshic-card{border-radius:12px;box-shadow:0 1px 2px rgba(0,0,0,.06);}
|
|
1290
|
+
.dshic-mini{border-radius:7px;}
|
|
1291
|
+
.dshic-toolbar{backdrop-filter:blur(6px);}
|
|
1292
|
+
.dshic-node .dshic-textarea,.dshic-node .dshic-input{border-radius:8px;}
|
|
1293
|
+
.dshic-preview{border-radius:10px;}
|
|
1259
1294
|
`;
|
|
1260
1295
|
|
|
1261
1296
|
// ============================================================
|
|
@@ -1593,7 +1628,36 @@ window.__ModuleLoader__.load({
|
|
|
1593
1628
|
if (entities && entities.length) return entityLabelOf(entities[0].kind) + '-' + entities[0].name + ' ' + nowStamp();
|
|
1594
1629
|
return (fallbackKind === 'video' ? '视频' : '图片') + '-' + nowStamp();
|
|
1595
1630
|
}
|
|
1596
|
-
|
|
1631
|
+
/** 修复未闭合的 @引用:模型在句尾常丢闭合 @(写作 @糯糯。 而非 @糯糯@。)——
|
|
1632
|
+
* 未闭合引用不会被词法识别,参考图会静默丢失。这里按已知资产名自动补全并归一为完整名;
|
|
1633
|
+
* 无法对上已知名的裸 @ 原样保留(不误伤普通文本)。 */
|
|
1634
|
+
function repairUnclosedMentions(text, knownNames) {
|
|
1635
|
+
const t = String(text || '');
|
|
1636
|
+
const names = (knownNames || []).filter((x) => x && String(x).trim()).map((x) => String(x).trim());
|
|
1637
|
+
if (!t || !names.length) return t;
|
|
1638
|
+
const stop = /[\s@,。;:、!?…()【】\[\]"'“”<>《》/\\|]/;
|
|
1639
|
+
let out = '';
|
|
1640
|
+
let i = 0;
|
|
1641
|
+
while (i < t.length) {
|
|
1642
|
+
if (t[i] !== '@') { out += t[i]; i++; continue; }
|
|
1643
|
+
let j = i + 1;
|
|
1644
|
+
while (j < t.length && !stop.test(t[j])) j++;
|
|
1645
|
+
const name = t.slice(i + 1, j);
|
|
1646
|
+
if (!name) { out += t[i]; i++; continue; }
|
|
1647
|
+
if (t[j] === '@') { out += t.slice(i, j + 1); i = j + 1; continue; } // 已闭合:原样
|
|
1648
|
+
const hit = names.find((k) => k === name || name.startsWith(k) || k.startsWith(name));
|
|
1649
|
+
if (hit) { out += '@' + hit + '@'; i = j; continue; } // 补闭合 + 归一为完整名
|
|
1650
|
+
out += t.slice(i, j + 1); i = j + 1; // 未命中:原样保留(含终止符)
|
|
1651
|
+
}
|
|
1652
|
+
return out;
|
|
1653
|
+
}
|
|
1654
|
+
/** 设定集里的全部资产名(角色/场景/道具),供未闭合引用修复与一致性检查使用。 */
|
|
1655
|
+
function bibleNames(bible) {
|
|
1656
|
+
const b = bible || {};
|
|
1657
|
+
return [].concat(b.characters || [], b.scenes || [], b.props || [])
|
|
1658
|
+
.map((x) => (x && x.name ? String(x.name).trim() : '')).filter(Boolean);
|
|
1659
|
+
}
|
|
1660
|
+
// <<<MENTION_PURE_END>>>
|
|
1597
1661
|
|
|
1598
1662
|
/** 解析提示词里的 @引用:返回命中实体 + 其名下的参考图资产与**声音参考资产**(按实体出现顺序、各实体内按生成时间)。
|
|
1599
1663
|
* 实体的参考资产来源:① 设定条目里**链接的资产**(assetIds,绑定关系优先)
|
|
@@ -2254,6 +2318,7 @@ window.__ModuleLoader__.load({
|
|
|
2254
2318
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:视频任务含排队可能很久,查询层绝不先放弃
|
|
2255
2319
|
while (Date.now() < deadline) {
|
|
2256
2320
|
await sleep(10000);
|
|
2321
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2257
2322
|
let r2 = null;
|
|
2258
2323
|
try { r2 = await fetchViaProxy(base + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询 Seedance 任务' }); } catch { /* 网络抖动重试 */ }
|
|
2259
2324
|
if (!r2 || !r2.ok) continue; // 查询层任何失败(网关 5xx/429/断连)都不算任务失败——只等生成任务自身的终态
|
|
@@ -2304,6 +2369,7 @@ window.__ModuleLoader__.load({
|
|
|
2304
2369
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:查询层绝不先放弃
|
|
2305
2370
|
while (Date.now() < deadline) {
|
|
2306
2371
|
await sleep(6000);
|
|
2372
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2307
2373
|
let r2 = null;
|
|
2308
2374
|
try { r2 = await fetchViaProxy(queryEp + '?task_id=' + encodeURIComponent(taskId), { method: 'GET', headers, timeoutMs: 60000, label: '查询视频任务' }); } catch { /* 网络抖动重试 */ }
|
|
2309
2375
|
if (!r2 || !r2.ok) continue; // 查询层失败 ≠ 任务失败,只等生成任务自身的终态
|
|
@@ -2340,6 +2406,7 @@ window.__ModuleLoader__.load({
|
|
|
2340
2406
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:查询层绝不先放弃
|
|
2341
2407
|
while (Date.now() < deadline) {
|
|
2342
2408
|
await sleep(6000);
|
|
2409
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2343
2410
|
let r2 = null;
|
|
2344
2411
|
try { r2 = await fetchViaProxy(ep + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询视频任务' }); } catch { /* 网络抖动重试 */ }
|
|
2345
2412
|
if (!r2 || !r2.ok) continue; // 查询层失败 ≠ 任务失败,只等生成任务自身的终态
|
|
@@ -2480,6 +2547,7 @@ window.__ModuleLoader__.load({
|
|
|
2480
2547
|
const deadline = Date.now() + 120 * 60 * 1000;
|
|
2481
2548
|
while (Date.now() < deadline) {
|
|
2482
2549
|
await sleep(step);
|
|
2550
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2483
2551
|
let r = null;
|
|
2484
2552
|
try { r = await query(); } catch { continue; }
|
|
2485
2553
|
if (!r) continue;
|
|
@@ -2913,6 +2981,7 @@ window.__ModuleLoader__.load({
|
|
|
2913
2981
|
const deadline = Date.now() + 120 * 60 * 1000; // 2 小时:查询层绝不先放弃
|
|
2914
2982
|
while (Date.now() < deadline) {
|
|
2915
2983
|
await sleep(3000);
|
|
2984
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
2916
2985
|
let r2 = null;
|
|
2917
2986
|
try { r2 = await fetchViaProxy(base + '/' + taskId, { method: 'GET', headers, timeoutMs: 60000, label: '查询视频任务' }); } catch { /* 网络抖动重试 */ }
|
|
2918
2987
|
if (!r2 || !r2.ok) continue; // 查询层失败 ≠ 任务失败,只等生成任务自身的终态
|
|
@@ -3970,8 +4039,10 @@ window.__ModuleLoader__.load({
|
|
|
3970
4039
|
method: 'POST',
|
|
3971
4040
|
headers: { 'content-type': 'application/json' },
|
|
3972
4041
|
body: JSON.stringify(args),
|
|
4042
|
+
signal: genAbort.ctl ? genAbort.ctl.signal : undefined, // 停止按钮:立即中断流式读取
|
|
3973
4043
|
});
|
|
3974
4044
|
} catch (e) {
|
|
4045
|
+
if (genAborted()) throw e; // 手动打断:保持 abort 语义供上层识别
|
|
3975
4046
|
throw new Error(T('connectFailed') + ':' + String((e && e.message) || e));
|
|
3976
4047
|
}
|
|
3977
4048
|
if (res.status === 404 || !res.body) return await aiComplete({ system, user, model }); // 旧 Host 回退
|
|
@@ -3999,6 +4070,7 @@ window.__ModuleLoader__.load({
|
|
|
3999
4070
|
else if (msg.t === 'fail') throw new Error(String(msg.d || T('rpcFail')));
|
|
4000
4071
|
}
|
|
4001
4072
|
}
|
|
4073
|
+
if (genAborted()) throw new Error(T('stoppedNote')); // 流结束后仍被打断 → 走打断路径
|
|
4002
4074
|
if (!final) throw new Error(T('rpcFail'));
|
|
4003
4075
|
return final.text || '';
|
|
4004
4076
|
}
|
|
@@ -4209,6 +4281,7 @@ window.__ModuleLoader__.load({
|
|
|
4209
4281
|
onReasoning: (d) => onDelta && onDelta('reasoning', d),
|
|
4210
4282
|
onText: (d) => onDelta && onDelta('text', d),
|
|
4211
4283
|
});
|
|
4284
|
+
if (genAborted()) throw new Error(T('stoppedNote'));
|
|
4212
4285
|
// 解析 + 修复重试:模型常把剧本里的引用文字(如 喷漆"拆"字)原样写进 JSON 字符串值,
|
|
4213
4286
|
// 半角引号未转义 → JSON 非法。修复策略:把"闭合引号后不跟 JSON 结构符"的引号对换成「」。
|
|
4214
4287
|
let parsed = parseJsonBalanced(text);
|
|
@@ -4877,6 +4950,13 @@ window.__ModuleLoader__.load({
|
|
|
4877
4950
|
// 工作流执行
|
|
4878
4951
|
// ============================================================
|
|
4879
4952
|
const cancelRef = { cancelled: false };
|
|
4953
|
+
const genAbort = { ctl: null }; // 当前生成的 AbortController(停止按钮真正中断底层流/轮询)
|
|
4954
|
+
let genActive = 0; // 活跃生成计数(工作流与单节点触发统一计数)
|
|
4955
|
+
function genAborted() { return !!(genAbort.ctl && genAbort.ctl.signal && genAbort.ctl.signal.aborted); }
|
|
4956
|
+
function stopActiveGen() {
|
|
4957
|
+
cancelRef.cancelled = true;
|
|
4958
|
+
if (genAbort.ctl) { try { genAbort.ctl.abort(); } catch { /* 忽略 */ } }
|
|
4959
|
+
}
|
|
4880
4960
|
|
|
4881
4961
|
/** 分镜节点的剧本来源:**连线上游与手动粘贴二选一**——
|
|
4882
4962
|
* 连了线就只用连线(上游没生成完会明确报错,绝不偷用粘贴的文本混着来)。 */
|
|
@@ -4922,10 +5002,11 @@ window.__ModuleLoader__.load({
|
|
|
4922
5002
|
try {
|
|
4923
5003
|
const text = await aiScriptNodes(n.data.idea, n.data.model, (kind, d) => aiStreamAppend(n.id, kind, d));
|
|
4924
5004
|
if (!String(text || '').trim()) throw new Error(T('aiEmptyResult'));
|
|
4925
|
-
|
|
5005
|
+
const fixedText = repairUnclosedMentions(text, bibleNames(loadBible(S.activeId)));
|
|
5006
|
+
update((s) => { const nd = s.graph.nodes[n.id]; if (nd) { nd.data.output = fixedText; nd.data.prevOutput = null; nd.data.aiStatus = 'idle'; nd.data.status = 'done'; nd.data.error = null; } s.assetsTick++; });
|
|
4926
5007
|
await saveTextAssetRecord(S.activeId, {
|
|
4927
5008
|
name: (String(n.data.label || '').trim() || T('scriptAssetPrefix')) + ' ' + nowStamp(),
|
|
4928
|
-
text, category: 'script', prompt: n.data.idea,
|
|
5009
|
+
text: fixedText, category: 'script', prompt: n.data.idea,
|
|
4929
5010
|
});
|
|
4930
5011
|
update((s) => { s.assetsTick++; });
|
|
4931
5012
|
flushSave();
|
|
@@ -4966,6 +5047,13 @@ window.__ModuleLoader__.load({
|
|
|
4966
5047
|
try {
|
|
4967
5048
|
const prev = prevSceneTail(n); // 场 N-1 的尾镜 → 作为本场第 1 镜的硬性承接约束
|
|
4968
5049
|
const shots = await aiStoryboardShots(st, n.data.requirement, n.data.count || 6, n.data.model, (kind, d) => aiStreamAppend(n.id, kind, d), prev);
|
|
5050
|
+
// 未闭合引用自愈:句尾丢闭合 @ 的引用按设定集名补全(否则参考图静默丢失)
|
|
5051
|
+
const _known = bibleNames(loadBible(S.activeId));
|
|
5052
|
+
for (const sh of shots) {
|
|
5053
|
+
sh.visual = repairUnclosedMentions(sh.visual, _known);
|
|
5054
|
+
sh.motion = repairUnclosedMentions(sh.motion, _known);
|
|
5055
|
+
sh.sfx = repairUnclosedMentions(sh.sfx, _known);
|
|
5056
|
+
}
|
|
4969
5057
|
// 先自动补录缺失条目(空描述),再算一致性告警:否则告警标志的条目其实马上就被自动补录了,"未登记"语义会误导用户与对话侧
|
|
4970
5058
|
try { await autoRegisterBibleEntries(shots); } catch { /* 补录失败不影响分镜 */ }
|
|
4971
5059
|
const warnings = await checkShotConsistency(shots);
|
|
@@ -5062,9 +5150,13 @@ window.__ModuleLoader__.load({
|
|
|
5062
5150
|
async function runGenNode(n) {
|
|
5063
5151
|
const type = n.type;
|
|
5064
5152
|
const settings = loadSettings(); // 模型解析(供应商+模型)用
|
|
5153
|
+
const ctl = new AbortController();
|
|
5154
|
+
genAbort.ctl = ctl;
|
|
5155
|
+
genActive++;
|
|
5065
5156
|
update((s) => {
|
|
5066
5157
|
const nd = s.graph.nodes[n.id];
|
|
5067
|
-
if (nd) { nd.data.status = 'running'; nd.data.error = null; }
|
|
5158
|
+
if (nd) { nd.data.status = 'running'; nd.data.error = null; nd.data.stopNote = null; }
|
|
5159
|
+
s.running = true; // 单节点触发(对话/节点按钮)也显示停止按钮
|
|
5068
5160
|
});
|
|
5069
5161
|
flushSave(); // 立刻落盘 + 推送 Host:切走画面/对话查询都能看到"运行中"
|
|
5070
5162
|
try {
|
|
@@ -5072,8 +5164,11 @@ window.__ModuleLoader__.load({
|
|
|
5072
5164
|
if (type === 'storyboard') { await genStoryboardNode(n); return true; }
|
|
5073
5165
|
if (type === 'finalcut') { await genFinalCutNode(n); return true; }
|
|
5074
5166
|
if (type !== 'image-gen' && type !== 'video-gen') throw new Error(T('nodeNotRunnable'));
|
|
5075
|
-
|
|
5167
|
+
let prompt = resolvePrompt(n);
|
|
5076
5168
|
const bible = loadBible(S.activeId);
|
|
5169
|
+
// 未闭合引用自愈:@糯糯。 → @糯糯@(按设定集名归一),修复结果回写节点让用户可见
|
|
5170
|
+
const _fixed = repairUnclosedMentions(prompt, bibleNames(bible));
|
|
5171
|
+
if (_fixed !== prompt) { prompt = _fixed; update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.prompt = _fixed; }); }
|
|
5077
5172
|
if (type === 'image-gen') {
|
|
5078
5173
|
// 参考图 = 连线(用户顺序)+ @引用命中的设定集资产(按出现顺序)
|
|
5079
5174
|
const { refs, entities } = await collectRefs(n, prompt, bible);
|
|
@@ -5152,12 +5247,28 @@ window.__ModuleLoader__.load({
|
|
|
5152
5247
|
}
|
|
5153
5248
|
return true;
|
|
5154
5249
|
} catch (err) {
|
|
5250
|
+
const stopped = !!(ctl.signal && ctl.signal.aborted);
|
|
5155
5251
|
update((s) => {
|
|
5156
5252
|
const nd = s.graph.nodes[n.id];
|
|
5157
|
-
if (nd)
|
|
5253
|
+
if (!nd) return;
|
|
5254
|
+
if (stopped) {
|
|
5255
|
+
// 手动打断:回到待命而不是报错;视频任务 id 已持久化,可凭 id 回查结果
|
|
5256
|
+
nd.data.status = 'idle';
|
|
5257
|
+
nd.data.error = null;
|
|
5258
|
+
if (nd.type === 'script' || nd.type === 'storyboard') nd.data.aiStatus = 'idle';
|
|
5259
|
+
nd.data.stopNote = T('stoppedNote') + (nd.data.taskId ? ';' + T('stoppedTaskKept') : '');
|
|
5260
|
+
} else {
|
|
5261
|
+
nd.data.status = 'error';
|
|
5262
|
+
nd.data.error = String((err && err.message) || err);
|
|
5263
|
+
}
|
|
5158
5264
|
});
|
|
5159
5265
|
flushSave(); // 让对话侧的 canvas_node_status 也能读到失败原因
|
|
5266
|
+
if (stopped) toast(T('stoppedToast'));
|
|
5160
5267
|
return false;
|
|
5268
|
+
} finally {
|
|
5269
|
+
genActive = Math.max(0, genActive - 1);
|
|
5270
|
+
if (genActive === 0) update((s) => { s.running = false; });
|
|
5271
|
+
if (genAbort.ctl === ctl) genAbort.ctl = null;
|
|
5161
5272
|
}
|
|
5162
5273
|
}
|
|
5163
5274
|
function commitGenResult(nodeId, assetId, demo, kind) {
|
|
@@ -5209,7 +5320,10 @@ window.__ModuleLoader__.load({
|
|
|
5209
5320
|
flushSave();
|
|
5210
5321
|
}
|
|
5211
5322
|
}
|
|
5212
|
-
function stopRun() {
|
|
5323
|
+
function stopRun() {
|
|
5324
|
+
stopActiveGen();
|
|
5325
|
+
toast(T('stoppedToast'));
|
|
5326
|
+
}
|
|
5213
5327
|
|
|
5214
5328
|
// ============================================================
|
|
5215
5329
|
// 轻量提示
|
|
@@ -5370,6 +5484,9 @@ window.__ModuleLoader__.load({
|
|
|
5370
5484
|
el.style.top = ny + 'px';
|
|
5371
5485
|
movedIds.add(st.id);
|
|
5372
5486
|
}
|
|
5487
|
+
// —— 实时碰撞推挤(物理感):拖动节点碰到谁,谁沿远离方向实时让位;被推者再推别人(级联)。
|
|
5488
|
+
// 上下左右同理:下节点上移碰到上节点 → 上节点实时上移;全程无弹跳,松手即定格。
|
|
5489
|
+
resolvePushNow(movedIds);
|
|
5373
5490
|
if (movedIds.size) {
|
|
5374
5491
|
for (const e2 of S.graph.edges) {
|
|
5375
5492
|
if (!movedIds.has(e2.fromNode) && !movedIds.has(e2.toNode)) continue;
|
|
@@ -5582,24 +5699,90 @@ window.__ModuleLoader__.load({
|
|
|
5582
5699
|
});
|
|
5583
5700
|
const TYPE_KIND = { 'text': 'text', 'shot': 'shot', 'script': 'script', 'storyboard': 'shot', 'image-gen': 'image', 'video-gen': 'video', 'image-asset': 'image', 'video-asset': 'video', 'finalcut': 'video', 'group': 'text' };
|
|
5584
5701
|
|
|
5585
|
-
/**
|
|
5702
|
+
/** 节点框相交判定(含最小间距,默认 24px)。 */
|
|
5703
|
+
/** 实时碰撞推挤共享核心(拖动 / 节点尺寸变化共用):与 movers 里节点相交的其他节点,
|
|
5704
|
+
* 沿远离主动节点的方向让位(重叠较少的轴先动,上下左右机会均等),被推者再推别人,级联最多 4 轮。
|
|
5705
|
+
* 高频路径:直接写模型坐标并同步被推节点的 DOM(不触发 React 重渲染),由调用方在静止时收尾落盘。 */
|
|
5706
|
+
function resolvePushNow(movers) {
|
|
5707
|
+
const PUSH_GAP = 8; // 触距:小于静止间距,贴近"贴上就推"的手感
|
|
5708
|
+
let frontier = [...movers];
|
|
5709
|
+
for (let pass = 0; pass < 4 && frontier.length; pass++) {
|
|
5710
|
+
const next = [];
|
|
5711
|
+
for (const fid of frontier) {
|
|
5712
|
+
const a = S.graph.nodes[fid];
|
|
5713
|
+
if (!a) continue;
|
|
5714
|
+
const al = a.x, ar = a.x + (a.w || 240), at = a.y, ab = a.y + (a.h || 96);
|
|
5715
|
+
for (const other of Object.values(S.graph.nodes)) {
|
|
5716
|
+
if (!other || movers.has(other.id) || other.type === 'group' || (other.data && other.data.hidden)) continue;
|
|
5717
|
+
const bl = other.x, br = other.x + (other.w || 240), bt = other.y, bb = other.y + (other.h || 96);
|
|
5718
|
+
if (al >= br + PUSH_GAP || ar + PUSH_GAP <= bl || at >= bb + PUSH_GAP || ab + PUSH_GAP <= bt) continue; // 未相交
|
|
5719
|
+
// 沿重叠较小的轴推开,方向 = 远离主动节点
|
|
5720
|
+
const penX = Math.min(ar, br) - Math.max(al, bl);
|
|
5721
|
+
const penY = Math.min(ab, bb) - Math.max(at, bt);
|
|
5722
|
+
const acx = (al + ar) / 2, acy = (at + ab) / 2;
|
|
5723
|
+
const bcx = (bl + br) / 2, bcy = (bt + bb) / 2;
|
|
5724
|
+
if (penX <= penY) {
|
|
5725
|
+
other.x = Math.round(bcx >= acx ? ar + PUSH_GAP : al - (br - bl) - PUSH_GAP);
|
|
5726
|
+
} else {
|
|
5727
|
+
other.y = Math.round(bcy >= acy ? ab + PUSH_GAP : at - (bb - bt) - PUSH_GAP);
|
|
5728
|
+
}
|
|
5729
|
+
const oel = nodeEls.get(other.id);
|
|
5730
|
+
if (oel) { oel.style.left = other.x + 'px'; oel.style.top = other.y + 'px'; }
|
|
5731
|
+
movers.add(other.id);
|
|
5732
|
+
next.push(other.id);
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
frontier = next;
|
|
5736
|
+
}
|
|
5737
|
+
return movers;
|
|
5738
|
+
}
|
|
5739
|
+
|
|
5740
|
+
/** 节点尺寸自适应:RO 上报最新尺寸(拖节点边、拉长内容输入框、流式长高、宽度变化都算),
|
|
5741
|
+
* 尺寸一变就实时推开相交的邻居(resolvePushNow,与拖动碰撞同一套物理推挤,所有节点类型统一走这里);
|
|
5742
|
+
* 静止约 260ms 后收尾:通知 React + 记撤销快照 + 落盘。
|
|
5743
|
+
* 分组框高度由创建/折叠样式决定,不采集;首次测量只登记尺寸,避免载入时画面自行跳动。 */
|
|
5586
5744
|
function observeNodeHeight(id, el) {
|
|
5587
5745
|
if (!el) return;
|
|
5746
|
+
let raf = 0;
|
|
5747
|
+
let settleTimer = null;
|
|
5748
|
+
let touched = false;
|
|
5749
|
+
const scheduleSettle = () => {
|
|
5750
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
5751
|
+
settleTimer = setTimeout(() => {
|
|
5752
|
+
settleTimer = null;
|
|
5753
|
+
if (!touched) return;
|
|
5754
|
+
touched = false;
|
|
5755
|
+
notify();
|
|
5756
|
+
pushHistory();
|
|
5757
|
+
flushSave();
|
|
5758
|
+
}, 260);
|
|
5759
|
+
};
|
|
5588
5760
|
const ro = new ResizeObserver(() => {
|
|
5589
5761
|
const nd = S.graph.nodes[id];
|
|
5590
5762
|
if (!nd || nd.type === 'group') return;
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5763
|
+
const w = el.offsetWidth, h = el.offsetHeight;
|
|
5764
|
+
if (!w || !h) return;
|
|
5765
|
+
if (Math.abs((nd.h || 0) - h) <= 1 && Math.abs((nd.w || 0) - w) <= 1) return;
|
|
5766
|
+
const first = !(nd.h || 0) && !(nd.w || 0);
|
|
5767
|
+
nd.w = w; nd.h = h;
|
|
5768
|
+
for (const e of S.graph.edges) {
|
|
5769
|
+
if (e.fromNode === id || e.toNode === id) {
|
|
5770
|
+
const p = edgePathEls.get(e.id);
|
|
5771
|
+
if (p) p.setAttribute('d', edgePath(e));
|
|
5598
5772
|
}
|
|
5599
5773
|
}
|
|
5774
|
+
if (!first && !raf) {
|
|
5775
|
+
raf = requestAnimationFrame(() => {
|
|
5776
|
+
raf = 0;
|
|
5777
|
+
try {
|
|
5778
|
+
const pushed = resolvePushNow(new Set([id]));
|
|
5779
|
+
if (pushed.size > 1) { touched = true; scheduleSettle(); }
|
|
5780
|
+
} catch { /* 布局自愈失败不影响内容 */ }
|
|
5781
|
+
});
|
|
5782
|
+
}
|
|
5600
5783
|
});
|
|
5601
5784
|
ro.observe(el);
|
|
5602
|
-
return () => ro.disconnect();
|
|
5785
|
+
return () => { ro.disconnect(); if (raf) cancelAnimationFrame(raf); if (settleTimer) clearTimeout(settleTimer); };
|
|
5603
5786
|
}
|
|
5604
5787
|
|
|
5605
5788
|
function PortEl({ n, port, dir, corner }) {
|
|
@@ -5726,6 +5909,8 @@ window.__ModuleLoader__.load({
|
|
|
5726
5909
|
const up = () => {
|
|
5727
5910
|
window.removeEventListener('pointermove', move);
|
|
5728
5911
|
window.removeEventListener('pointerup', up);
|
|
5912
|
+
flushSave(); // 拖拽结束立即落盘:否则防抖期间切换页面会丢失 w/h/hManual
|
|
5913
|
+
try { if (resolvePushNow(new Set([n.id])).size > 1) notify(); } catch { /* 布局自愈失败不影响尺寸 */ }
|
|
5729
5914
|
pushHistory();
|
|
5730
5915
|
};
|
|
5731
5916
|
window.addEventListener('pointermove', move);
|
|
@@ -6199,22 +6384,24 @@ window.__ModuleLoader__.load({
|
|
|
6199
6384
|
const assetUrlV = useAssetUrl(n.data.assetId);
|
|
6200
6385
|
return [
|
|
6201
6386
|
promptEdge
|
|
6202
|
-
? h(
|
|
6203
|
-
key: 'p',
|
|
6204
|
-
|
|
6205
|
-
|
|
6387
|
+
? h(PromptArea, {
|
|
6388
|
+
key: 'p', value: n.data.prompt || '', minHeight: 72, readOnly: true,
|
|
6389
|
+
...phBind(n, 'p'),
|
|
6390
|
+
placeholder: fromStoryboard ? T('multiShotHint') : T('connected'),
|
|
6391
|
+
onChange: () => { /* 只读展示:内容由上游连线组合 */ },
|
|
6206
6392
|
})
|
|
6207
6393
|
: h(PromptArea, {
|
|
6208
6394
|
key: 'p', value: n.data.prompt || '', minHeight: 72,
|
|
6395
|
+
...phBind(n, 'p'),
|
|
6209
6396
|
placeholder: T('promptPlaceholder'),
|
|
6210
6397
|
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.prompt = v; }),
|
|
6211
6398
|
}),
|
|
6212
6399
|
h(GenSettingsRow, { key: 'set', n, kind }),
|
|
6213
6400
|
h('button', {
|
|
6214
|
-
key: 'gen', type: 'button', className: 'dshic-gen',
|
|
6401
|
+
key: 'gen', type: 'button', className: 'dshic-gen',
|
|
6215
6402
|
onPointerDown: (e) => e.stopPropagation(),
|
|
6216
|
-
onClick: () => { runGenNode(n); },
|
|
6217
|
-
}, n.data.status === 'running' ?
|
|
6403
|
+
onClick: () => { if (n.data.status === 'running') { stopActiveGen(); toast(T('stoppedToast')); } else { runGenNode(n); } },
|
|
6404
|
+
}, n.data.status === 'running' ? [Ic('stop', 12), T('stop')] : [Ic('play', 12), T('generate')]),
|
|
6218
6405
|
h('div', { key: 'r', className: 'dshic-node-row' },
|
|
6219
6406
|
h('div', { className: 'dshic-flex' }),
|
|
6220
6407
|
n.data.demo && n.data.assetId ? h('span', { className: 'dshic-tag demo' }, T('demoTag')) : null,
|
|
@@ -6243,6 +6430,7 @@ window.__ModuleLoader__.load({
|
|
|
6243
6430
|
h(RefRows, { key: 'refrows', n, portId: 'ref' }),
|
|
6244
6431
|
h(MentionInfo, { key: 'mn', n }),
|
|
6245
6432
|
n.data.genWarn ? h('div', { key: 'gw', className: 'dshic-hint', style: { color: 'var(--dsw-alias-state-warning-primary, #d08700)' } }, '⚠ ' + n.data.genWarn) : null,
|
|
6433
|
+
n.data.stopNote ? h('div', { key: 'sn', className: 'dshic-hint' }, '⏹ ' + n.data.stopNote) : null,
|
|
6246
6434
|
n.data.error ? h('div', { key: 'err', className: 'dshic-err' }, n.data.error) : null,
|
|
6247
6435
|
assetUrlV && kind === 'image'
|
|
6248
6436
|
? h('img', { key: 'img', className: 'dshic-preview', src: assetUrlV, draggable: false, 'data-no-drag': '1', style: { cursor: 'zoom-in' }, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => { e.stopPropagation(); openLightbox('image', assetUrlV, n.data.label); } })
|
|
@@ -6518,6 +6706,7 @@ window.__ModuleLoader__.load({
|
|
|
6518
6706
|
value: n.data.text || '',
|
|
6519
6707
|
placeholder: T('textPlaceholder'),
|
|
6520
6708
|
minHeight: 96,
|
|
6709
|
+
...phBind(n, 'text'),
|
|
6521
6710
|
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.text = v; }),
|
|
6522
6711
|
});
|
|
6523
6712
|
}
|
|
@@ -6544,37 +6733,100 @@ window.__ModuleLoader__.load({
|
|
|
6544
6733
|
refLines.map((x) => x.lb + ':' + x.names.join('、')).join(' | '))
|
|
6545
6734
|
: null,
|
|
6546
6735
|
h('span', { className: 'dshic-shot-label' }, T('visualLabel')),
|
|
6547
|
-
h(
|
|
6548
|
-
|
|
6736
|
+
h(PromptArea, {
|
|
6737
|
+
value: n.data.visual || '', minHeight: 52,
|
|
6738
|
+
...phBind(n, 'visual'),
|
|
6549
6739
|
placeholder: T('shotVisualPlaceholder'),
|
|
6550
|
-
onChange: (
|
|
6551
|
-
onPointerDown: (e) => e.stopPropagation(),
|
|
6740
|
+
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.visual = v; }),
|
|
6552
6741
|
}),
|
|
6553
6742
|
h('span', { className: 'dshic-shot-label' }, T('motionLabel')),
|
|
6554
|
-
h(
|
|
6555
|
-
|
|
6743
|
+
h(PromptArea, {
|
|
6744
|
+
value: n.data.motion || '', minHeight: 44,
|
|
6745
|
+
...phBind(n, 'motion'),
|
|
6556
6746
|
placeholder: T('shotMotionPlaceholder'),
|
|
6557
|
-
onChange: (
|
|
6558
|
-
onPointerDown: (e) => e.stopPropagation(),
|
|
6747
|
+
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.motion = v; }),
|
|
6559
6748
|
}),
|
|
6560
6749
|
);
|
|
6561
6750
|
}
|
|
6562
6751
|
|
|
6752
|
+
/** 文本框独立拉高的记忆:PromptArea 的原生 grip 拖高写进节点 data.ph[key],
|
|
6753
|
+
* 换页/刷新重挂载后原样恢复(不再退回 minHeight)。key 在同一节点内需唯一。 */
|
|
6754
|
+
function phBind(n, key) {
|
|
6755
|
+
return {
|
|
6756
|
+
phH: (n.data.ph && n.data.ph[key]) || null,
|
|
6757
|
+
onPh: (px) => update((s) => {
|
|
6758
|
+
const nd = s.graph.nodes[n.id];
|
|
6759
|
+
if (!nd) return;
|
|
6760
|
+
nd.data.ph = { ...(nd.data.ph || {}), [key]: px };
|
|
6761
|
+
}),
|
|
6762
|
+
// 手动高度节点里拖原生 grip 拉对话框:视觉高度被 flex 定死,但 inline height 的增量=拖动量,
|
|
6763
|
+
// 换算成节点 hManual 的增量 → 对话框随节点一起变长;自适应节点不需要(高度本来就跟内容走)。
|
|
6764
|
+
grow: (d) => update((s) => {
|
|
6765
|
+
const nd = s.graph.nodes[n.id];
|
|
6766
|
+
if (!nd || !d || !nd.hManual) return;
|
|
6767
|
+
nd.hManual = Math.max(96, Math.round(nd.hManual + d));
|
|
6768
|
+
nd.h = nd.hManual;
|
|
6769
|
+
}),
|
|
6770
|
+
};
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6563
6773
|
/**
|
|
6564
6774
|
* 提示词输入框:@名称 蓝色加粗高亮。
|
|
6565
6775
|
* 与主流工具同一做法——透明文字的 textarea 叠在同步滚动的背景层上,
|
|
6566
6776
|
* 背景层渲染同一段文本并给 @名称 上色,光标/选区/中文输入法都不受影响。
|
|
6567
6777
|
*/
|
|
6568
|
-
function PromptArea({ value, onChange, placeholder, minHeight, style }) {
|
|
6778
|
+
function PromptArea({ value, onChange, placeholder, minHeight, style, readOnly, phH, onPh, grow }) {
|
|
6569
6779
|
const taRef = useRef(null);
|
|
6570
6780
|
const backRef = useRef(null);
|
|
6781
|
+
// 记忆高度的基准:React 上一次写进 textarea 的 height(null=未设置)。
|
|
6782
|
+
// 原生 grip 拉高会直接改 inline style,与基准不同即视为用户改动 → 回调存进节点数据。
|
|
6783
|
+
const hBaseRef = useRef(null);
|
|
6784
|
+
// grip 拖动轨迹:inline height 的上一次值(手动高度节点用它把拖动量换算成节点高度增量)
|
|
6785
|
+
const lastInlineRef = useRef(null);
|
|
6786
|
+
useEffect(() => {
|
|
6787
|
+
hBaseRef.current = phH || null;
|
|
6788
|
+
lastInlineRef.current = phH || null;
|
|
6789
|
+
}, [phH]);
|
|
6571
6790
|
const sync = () => {
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6791
|
+
const ta = taRef.current;
|
|
6792
|
+
const back = backRef.current;
|
|
6793
|
+
if (!ta || !back) return;
|
|
6794
|
+
// 父级高度不定时绝对定位的 bottom 会失效,背层随文字撑高并画出边框。
|
|
6795
|
+
// 用输入框的实际高度锁死背层,滚动才对齐;外层 overflow:hidden 再兜底裁切。
|
|
6796
|
+
const h = ta.offsetHeight;
|
|
6797
|
+
if (h > 0 && back.style.height !== h + 'px') back.style.height = h + 'px';
|
|
6798
|
+
back.scrollTop = ta.scrollTop;
|
|
6799
|
+
back.scrollLeft = ta.scrollLeft;
|
|
6576
6800
|
};
|
|
6577
|
-
|
|
6801
|
+
useEffect(() => {
|
|
6802
|
+
sync();
|
|
6803
|
+
const ta = taRef.current;
|
|
6804
|
+
if (!ta || typeof ResizeObserver === 'undefined') return undefined;
|
|
6805
|
+
let hRaf = 0;
|
|
6806
|
+
const ro = new ResizeObserver(() => {
|
|
6807
|
+
sync();
|
|
6808
|
+
if (!onPh) return;
|
|
6809
|
+
const px = parseInt(ta.style.height, 10) || 0;
|
|
6810
|
+
if (px > 0 && px !== (hBaseRef.current || 0) && !hRaf) {
|
|
6811
|
+
hRaf = requestAnimationFrame(() => {
|
|
6812
|
+
hRaf = 0;
|
|
6813
|
+
const cur = parseInt(ta.style.height, 10) || 0;
|
|
6814
|
+
if (cur > 0 && cur !== (hBaseRef.current || 0)) {
|
|
6815
|
+
if (grow && lastInlineRef.current != null) grow(cur - lastInlineRef.current);
|
|
6816
|
+
lastInlineRef.current = cur;
|
|
6817
|
+
onPh(cur);
|
|
6818
|
+
}
|
|
6819
|
+
});
|
|
6820
|
+
}
|
|
6821
|
+
});
|
|
6822
|
+
ro.observe(ta);
|
|
6823
|
+
return () => { ro.disconnect(); if (hRaf) cancelAnimationFrame(hRaf); };
|
|
6824
|
+
}, []);
|
|
6825
|
+
// 值变化后补一次滚动同步:在末尾输入时 textarea 会自动滚动到光标,但背层不一定收到 scroll 事件
|
|
6826
|
+
useEffect(() => { sync(); }, [value]);
|
|
6827
|
+
// 结尾换行补偿:<textarea> 的结尾 \n 会多出一条空行,而 pre-wrap 的 <div> 不生成该行盒
|
|
6828
|
+
// → 两层 scrollHeight 差一行,滚到底时背层露出半截错位文字。补零宽字符让行盒存在。
|
|
6829
|
+
const html = highlightMentions(value || '').replace(/\n$/, '\n\u200b');
|
|
6578
6830
|
const ph = placeholder ? '<span class="dshic-prompt-ph">' + escapeHtml(placeholder) + '</span>' : '';
|
|
6579
6831
|
return h('div', { className: 'dshic-prompt' },
|
|
6580
6832
|
h('div', {
|
|
@@ -6584,7 +6836,8 @@ window.__ModuleLoader__.load({
|
|
|
6584
6836
|
h('textarea', {
|
|
6585
6837
|
ref: taRef, className: 'dshic-textarea dshic-prompt-input', value: value || '',
|
|
6586
6838
|
placeholder: html ? undefined : placeholder,
|
|
6587
|
-
|
|
6839
|
+
readOnly: readOnly === true,
|
|
6840
|
+
style: { minHeight: minHeight || 72, height: phH || undefined, ...(style || {}) },
|
|
6588
6841
|
onChange: (e) => onChange(e.target.value),
|
|
6589
6842
|
onScroll: sync,
|
|
6590
6843
|
onPointerDown: (e) => e.stopPropagation(),
|
|
@@ -6654,11 +6907,11 @@ window.__ModuleLoader__.load({
|
|
|
6654
6907
|
h('div', { className: 'dshic-flex' }),
|
|
6655
6908
|
),
|
|
6656
6909
|
h('span', { key: 'l1', className: 'dshic-shot-label' }, T('ideaLabel')),
|
|
6657
|
-
h(
|
|
6658
|
-
key: 'idea',
|
|
6910
|
+
h(PromptArea, {
|
|
6911
|
+
key: 'idea', value: n.data.idea || '', minHeight: 72,
|
|
6912
|
+
...phBind(n, 'idea'),
|
|
6659
6913
|
placeholder: T('ideaPlaceholder'),
|
|
6660
|
-
onChange: (
|
|
6661
|
-
onPointerDown: (e) => e.stopPropagation(),
|
|
6914
|
+
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.idea = v; }),
|
|
6662
6915
|
}),
|
|
6663
6916
|
h('button', { key: 'gen', type: 'button', className: 'dshic-gen', disabled: running, onClick: generate, onPointerDown: (e) => e.stopPropagation() },
|
|
6664
6917
|
running ? h('span', { className: 'dshic-spin' }) : Ic('spark', 12), running ? T('aiRunning') : T('aiScript')),
|
|
@@ -6685,11 +6938,11 @@ window.__ModuleLoader__.load({
|
|
|
6685
6938
|
),
|
|
6686
6939
|
n.data.aiError ? h('div', { key: 'err', className: 'dshic-err' }, n.data.aiError) : null,
|
|
6687
6940
|
h('span', { key: 'l2', className: 'dshic-shot-label' }, T('scriptOutputLabel')),
|
|
6688
|
-
h(
|
|
6689
|
-
key: 'out',
|
|
6941
|
+
h(PromptArea, {
|
|
6942
|
+
key: 'out', value: n.data.output || '', minHeight: 180,
|
|
6943
|
+
...phBind(n, 'out'),
|
|
6690
6944
|
placeholder: running ? T('aiRunning') : '',
|
|
6691
|
-
onChange: (
|
|
6692
|
-
onPointerDown: (e) => e.stopPropagation(),
|
|
6945
|
+
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.output = v; }),
|
|
6693
6946
|
}),
|
|
6694
6947
|
];
|
|
6695
6948
|
}
|
|
@@ -6701,13 +6954,36 @@ window.__ModuleLoader__.load({
|
|
|
6701
6954
|
const startResize = (e) => {
|
|
6702
6955
|
e.stopPropagation(); e.preventDefault();
|
|
6703
6956
|
const startY = e.clientY; const startH = listH; const startNodeH = Number(n.h) || 0;
|
|
6957
|
+
const startHManual = n.hManual ? Number(n.hManual) : 0;
|
|
6704
6958
|
const onMove = (ev) => {
|
|
6705
6959
|
const nh = Math.max(80, Math.min(1200, startH + (ev.clientY - startY)));
|
|
6706
6960
|
const dh = nh - startH;
|
|
6707
|
-
|
|
6708
|
-
|
|
6961
|
+
update((s) => {
|
|
6962
|
+
const nd = s.graph.nodes[n.id]; if (!nd) return;
|
|
6963
|
+
nd.data.shotsListH = nh;
|
|
6964
|
+
if (startHManual > 0) {
|
|
6965
|
+
// 手动高度节点:列表加多高,节点就长多高(否则固定高度会把上方输入框挤扁)
|
|
6966
|
+
nd.hManual = Math.max(120, startHManual + dh);
|
|
6967
|
+
nd.h = nd.hManual;
|
|
6968
|
+
} else if (startNodeH > 0) {
|
|
6969
|
+
nd.h = Math.max(120, startNodeH + dh);
|
|
6970
|
+
}
|
|
6971
|
+
});
|
|
6972
|
+
};
|
|
6973
|
+
const onUp = () => {
|
|
6974
|
+
window.removeEventListener('pointermove', onMove);
|
|
6975
|
+
window.removeEventListener('pointerup', onUp);
|
|
6976
|
+
try {
|
|
6977
|
+
// 收尾对齐:手动高度若还截着内容(此前被挤压过的存量节点),按内容自然高度补齐一次
|
|
6978
|
+
const el = nodeEls.get(n.id);
|
|
6979
|
+
const body = el && el.querySelector('.dshic-node-body');
|
|
6980
|
+
if (body && body.scrollHeight > body.clientHeight + 1) {
|
|
6981
|
+
const deficit = body.scrollHeight - body.clientHeight;
|
|
6982
|
+
update((s) => { const nd = s.graph.nodes[n.id]; if (nd && nd.hManual) nd.hManual = Math.round(nd.hManual + deficit); });
|
|
6983
|
+
}
|
|
6984
|
+
} catch { /* 布局自愈失败不影响功能 */ }
|
|
6985
|
+
flushSave();
|
|
6709
6986
|
};
|
|
6710
|
-
const onUp = () => { window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); flushSave(); };
|
|
6711
6987
|
window.addEventListener('pointermove', onMove);
|
|
6712
6988
|
window.addEventListener('pointerup', onUp);
|
|
6713
6989
|
};
|
|
@@ -6733,18 +7009,17 @@ window.__ModuleLoader__.load({
|
|
|
6733
7009
|
h('div', { className: 'dshic-flex' }),
|
|
6734
7010
|
),
|
|
6735
7011
|
h('span', { key: 'l1', className: 'dshic-shot-label' }, T('storyboardScriptLabel') + (scriptEdge ? ' · ' + T('connected') : '')),
|
|
6736
|
-
h(
|
|
6737
|
-
key: 'script',
|
|
7012
|
+
h(PromptArea, {
|
|
7013
|
+
key: 'script', value: n.data.script || '', minHeight: 56, readOnly: !!scriptEdge,
|
|
7014
|
+
...phBind(n, 'script'),
|
|
6738
7015
|
placeholder: scriptEdge ? T('connected') : T('storyboardScriptLabel'),
|
|
6739
|
-
|
|
6740
|
-
onChange: (e) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.script = e.target.value; }),
|
|
6741
|
-
onPointerDown: (e) => e.stopPropagation(),
|
|
7016
|
+
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.script = v; }),
|
|
6742
7017
|
}),
|
|
6743
|
-
h(
|
|
6744
|
-
key: 'req',
|
|
7018
|
+
h(PromptArea, {
|
|
7019
|
+
key: 'req', value: n.data.requirement || '', minHeight: 40,
|
|
7020
|
+
...phBind(n, 'req'),
|
|
6745
7021
|
placeholder: T('storyboardReqLabel'),
|
|
6746
|
-
onChange: (
|
|
6747
|
-
onPointerDown: (e) => e.stopPropagation(),
|
|
7022
|
+
onChange: (v) => update((s) => { const nd = s.graph.nodes[n.id]; if (nd) nd.data.requirement = v; }),
|
|
6748
7023
|
}),
|
|
6749
7024
|
h('div', { key: 'row', className: 'dshic-node-row' },
|
|
6750
7025
|
h(MiniSelect, {
|
package/index.js
CHANGED
|
@@ -787,23 +787,23 @@ function nextNodePos(doc, type, connectFromId) {
|
|
|
787
787
|
const EST_H = { storyboard: 560, 'video-gen': 620, 'image-gen': 560, script: 420, finalcut: 420, text: 220 };
|
|
788
788
|
const estH = EST_H[type] || 300;
|
|
789
789
|
const estW = type === 'finalcut' ? 300 : 280;
|
|
790
|
+
const same = nodes.filter((n) => n.type === type);
|
|
790
791
|
let x = 80;
|
|
791
792
|
let y = 40;
|
|
792
|
-
if (
|
|
793
|
+
if (same.length) {
|
|
794
|
+
// 一类型一列(布局铁律):该类型已有节点 → 永远追加到同类型列尾;
|
|
795
|
+
// 连线只表达数据流,不改变列归属(否则类型会交叉、画布无法辨认)
|
|
796
|
+
x = Math.min(...same.map((n) => n.x || 0));
|
|
797
|
+
y = Math.max(...same.map((n) => (n.y || 0) + H(n))) + GAP_Y;
|
|
798
|
+
} else if (connectFromId && doc.nodes && doc.nodes[connectFromId]) {
|
|
799
|
+
// 该类型的第一个节点:放在上游右侧一行(自然形成新的一列)
|
|
793
800
|
const src = doc.nodes[connectFromId];
|
|
794
801
|
x = (src.x || 0) + W(src) + GAP_X;
|
|
795
802
|
y = src.y || 40;
|
|
796
|
-
} else {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
x = Math.min(...same.map((n) => n.x || 0));
|
|
801
|
-
y = Math.max(...same.map((n) => (n.y || 0) + H(n))) + GAP_Y;
|
|
802
|
-
} else if (nodes.length) {
|
|
803
|
-
// 该类型还没有节点 → 在最右侧另起一列(绝不插进已有列里跟别的类型混排)
|
|
804
|
-
x = Math.max(...nodes.map((n) => (n.x || 0) + W(n))) + GAP_X;
|
|
805
|
-
y = Math.min(...nodes.map((n) => n.y || 0));
|
|
806
|
-
}
|
|
803
|
+
} else if (nodes.length) {
|
|
804
|
+
// 该类型还没有节点且无上游 → 在最右侧另起一列(绝不插进已有列里跟别的类型混排)
|
|
805
|
+
x = Math.max(...nodes.map((n) => (n.x || 0) + W(n))) + GAP_X;
|
|
806
|
+
y = Math.min(...nodes.map((n) => n.y || 40));
|
|
807
807
|
}
|
|
808
808
|
const hit = (yy) => nodes.find((n) => (n.x || 0) < x + estW + MIN_GAP && (n.x || 0) + W(n) + MIN_GAP > x
|
|
809
809
|
&& (n.y || 0) < yy + estH + MIN_GAP && (n.y || 0) + H(n) + MIN_GAP > yy);
|
|
@@ -813,8 +813,8 @@ function nextNodePos(doc, type, connectFromId) {
|
|
|
813
813
|
return { x, y };
|
|
814
814
|
}
|
|
815
815
|
// ---------- 内置技能(操作规程 + 创作方法论,约束并提升对话的画布创作)----------
|
|
816
|
-
// canvas-studio=操作硬约束;canvas-craft
|
|
817
|
-
const SKILL_NAMES = ['canvas-studio', 'canvas-craft'];
|
|
816
|
+
// canvas-studio=操作硬约束;canvas-craft=剧本/分镜/提示词创作方法论;canvas-layout=节点布局纪律
|
|
817
|
+
const SKILL_NAMES = ['canvas-studio', 'canvas-craft', 'canvas-layout'];
|
|
818
818
|
// = @deepseek-ai/dsh-skill 的 BUNDLED_SKILL_RANK(避免 import DSH 包;registry 要求 rank 必须是有限数字)
|
|
819
819
|
const SKILL_RANK = 600;
|
|
820
820
|
/** 解析 SKILL.md 的 YAML frontmatter(仅需 description 与正文)。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninkch/dsh-infinite-canvas",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.18",
|
|
4
4
|
"description": "Node-based short-drama studio for DeepSeek Harness: script → storyboard → assets → shots → voiced, subtitled final cut on an infinite canvas.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|