fix(canvas): 分镜脚本节点文字溢出与视频模型下拉异常 #84 (#85)

* fix(canvas): 分镜脚本节点文字溢出与视频模型下拉异常 #84

- CompactInput 补齐 overflow-y-auto/whitespace-pre-wrap/break-words,长文本不再横向溢出撑开列宽
- filterModelsByCapability 增加 modelCosts[].protocol 协议层回退,未显式配置 capability 时通过协议识别能力
- isVideoModelName 扩展 pika/runway/gen-3/hunyuan-video 等主流视频模型名关键字
- ModelPicker.options 保留当前选中 value,避免过滤后 options 为空触发跳转设置页

* fix(canvas): 模型过滤协议反向排除 + 分镜脚本节点模型选择提示 #84

- filterModelsByCapability 协议层提升至最高优先级,协议明确属于其他能力时直接排除,
  防止 video/image/audio 协议的模型被误标为 text 后混入文本下拉
- isVideoModelName 补充 cogvideo/mochi/latte/stable-video/svd/animatediff/ltx-video/minimax-video/abab-video 等关键字
- isImageModelName 补充 nano-banana/ideogram/recraft/playground/leonardo 等关键字
- 分镜脚本节点 ModelPicker 包裹 Tooltip,提示"脚本生成仅支持文本模型"

* fix(canvas): 节点标题截断修复 — flex min-width 约束 + 宽度边界 + hover tooltip

- NodeExternalHeader: 容器增加 max-w-full 双重约束,窄节点时不越界,宽节点时上限 240px
- NodeExternalHeader: 标题 span 增加 min-w-0,解决 flex min-width:auto 导致 truncate 失效
- 所有节点标题(绘图/技能/角色参考/音频/分镜脚本)增加 title 属性,hover 显示完整标题
- AudioNodeContent: 标题 span 增加 min-w-0,修复 flex 子项截断

* fix(canvas): 节点顶部标题真正截断 — JS 计算 maxWidth + overflow-hidden + flex-1

- NodeExternalHeader 容器: 移除 Tailwind 级联冲突的 max-w-[240px]/max-w-full,
  改用 JS 计算 Math.min(240, node.width) 作为 inline style maxWidth,
  从根本上解决两个 max-width 声明的优先级不确定问题
- 容器增加 overflow-hidden 作为第二道防线
- 内部 text/input/button 元素改用 flex-1 + min-w-0 替代固定 max-w-[Npx],
  让 flex 子项自动填充可用空间,文本在剩余空间内截断

* fix(canvas): 节点标题缩放溢出 — 考虑 canvas zoom scale

- maxHeaderWidth 改为 Math.min(240, node.width * scale),
  因为 header 的视觉宽度 = maxHeaderWidth(inverseScale 与 canvas scale 抵消),
  而节点的视觉宽度 = node.width * scale,两者必须比较同一维度
- 修复 zoom 缩小时(scale < 1)header 视觉宽度超过节点视觉宽度的溢出问题

---------

Co-authored-by: sikongyue <3155157671@qq.com>
This commit is contained in:
爱笑的毛毛虫
2026-08-03 08:40:04 +08:00
committed by GitHub
parent 9c910e8823
commit f8cb3d0201
4 changed files with 77 additions and 31 deletions

View File

@@ -129,7 +129,29 @@ export type ConfigStoreSnapshot = {
function isVideoModelName(model: string) {
const value = modelOptionName(model).toLowerCase();
return value.includes("seedance") || value.includes("video") || value.includes("sora") || value.includes("veo") || value.includes("kling") || value.includes("wan") || value.includes("hailuo");
return value.includes("seedance")
|| value.includes("video")
|| value.includes("sora")
|| value.includes("veo")
|| value.includes("kling")
|| value.includes("wan")
|| value.includes("hailuo")
|| value.includes("pika")
|| value.includes("runway")
|| value.includes("gen-3")
|| value.includes("gen3")
|| value.includes("hunyuan-video")
|| value.includes("hunyuanvideo")
|| value.includes("cogvideo")
|| value.includes("mochi")
|| value.includes("latte")
|| value.includes("stable-video")
|| value.includes("svd")
|| value.includes("animatediff")
|| value.includes("ltx-video")
|| value.includes("ltxvideo")
|| value.includes("minimax-video")
|| value.includes("abab-video");
}
function isImageModelName(model: string) {
@@ -146,7 +168,13 @@ function isImageModelName(model: string) {
value.includes("flux") ||
value.includes("sdxl") ||
value.includes("stable-diffusion") ||
value.includes("midjourney"))
value.includes("midjourney") ||
value.includes("nano-banana") ||
value.includes("nanobanana") ||
value.includes("ideogram") ||
value.includes("recraft") ||
value.includes("playground") ||
value.includes("leonardo"))
);
}
@@ -172,10 +200,20 @@ export function filterModelsByCapability(models: string[], capability?: ModelCap
return models.filter((model) => {
const decoded = decodeChannelModel(model);
const channel = decoded ? channels?.find((item) => item.id === decoded.channelId) : undefined;
const configuredCapability = channel?.modelCosts?.find((item) => item.model === decoded?.model)?.capability;
if (configuredCapability) return configuredCapability === capability;
const modelName = decoded?.model || modelOptionName(model);
const costEntry = channel?.modelCosts?.find((item) => item.model === modelName);
// 协议层优先级最高:协议决定 API 端点,明确属于其他能力时直接排除,
// 防止用户将 video/image/audio 协议的模型误标为 text 后混入文本下拉。
const protocolCapability = modelProtocolCapability(costEntry?.protocol);
if (protocolCapability) return protocolCapability === capability;
// 渠道接口层:渠道级协议推断能力
const channelCapability = capabilityForChannelInterface(channel?.interfaceType);
return channelCapability ? channelCapability === capability : modelMatchesCapability(model, capability);
if (channelCapability) return channelCapability === capability;
// 配置能力层:用户显式标记的 capability
const configuredCapability = costEntry?.capability;
if (configuredCapability) return configuredCapability === capability;
// 模型名启发式:最后回退
return modelMatchesCapability(model, capability);
});
}