mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +00:00
Compare commits
11 Commits
feat/heade
...
issue-5033
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11098f7f9b | ||
|
|
65785af348 | ||
|
|
ec4ad5ea92 | ||
|
|
e9ddf29507 | ||
|
|
fdd8564c07 | ||
|
|
d18081a54e | ||
|
|
45cc6ca2b4 | ||
|
|
c303a3f037 | ||
|
|
c90fd18ade | ||
|
|
2ed1704749 | ||
|
|
7d5a4d423e |
@@ -13,6 +13,10 @@ module.exports = defineConfig({
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
|
||||
|
||||
IMPORTANT Chinese Translation Guidelines:
|
||||
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
|
||||
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
|
||||
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
|
||||
`
|
||||
});
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.3",
|
||||
"version": "1.26.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.26.3",
|
||||
"version": "1.26.4",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.26.3",
|
||||
"version": "1.26.4",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
>
|
||||
<div class="shortcut-info flex-grow pr-4">
|
||||
<div class="shortcut-name text-sm font-medium">
|
||||
{{ command.label || command.id }}
|
||||
{{ t(`commands.${normalizeI18nKey(command.id)}.label`) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ComfyCommandImpl } from '@/stores/commandStore'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
|
||||
@@ -90,18 +90,16 @@ const closeDialog = () => {
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const addNode = (nodeDef: ComfyNodeDefImpl) => {
|
||||
if (!triggerEvent) {
|
||||
console.warn('The trigger event was undefined when addNode was called.')
|
||||
return
|
||||
}
|
||||
|
||||
const node = litegraphService.addNodeOnGraph(nodeDef, {
|
||||
pos: getNewNodeLocation()
|
||||
})
|
||||
|
||||
if (disconnectOnReset) {
|
||||
if (disconnectOnReset && triggerEvent) {
|
||||
canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)
|
||||
} else if (!triggerEvent) {
|
||||
console.warn('The trigger event was undefined when addNode was called.')
|
||||
}
|
||||
|
||||
disconnectOnReset = false
|
||||
|
||||
// Notify changeTracker - new step should be added
|
||||
|
||||
@@ -1362,6 +1362,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
return '$0.0004/$0.0016 per 1K tokens'
|
||||
} else if (model.includes('gpt-4.1')) {
|
||||
return '$0.002/$0.008 per 1K tokens'
|
||||
} else if (model.includes('gpt-5-nano')) {
|
||||
return '$0.00005/$0.0004 per 1K tokens'
|
||||
} else if (model.includes('gpt-5-mini')) {
|
||||
return '$0.00025/$0.002 per 1K tokens'
|
||||
} else if (model.includes('gpt-5')) {
|
||||
return '$0.00125/$0.01 per 1K tokens'
|
||||
}
|
||||
return 'Token-based'
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformS
|
||||
import { LGraphEventMode, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import { useWorkflowStore } from '@/stores/workflowStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
|
||||
@@ -27,6 +27,7 @@ export type MinimapOptionKey =
|
||||
export function useMinimap() {
|
||||
const settingStore = useSettingStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
@@ -147,7 +148,11 @@ export function useMinimap() {
|
||||
}
|
||||
|
||||
const canvas = computed(() => canvasStore.canvas)
|
||||
const graph = ref(app.canvas?.graph)
|
||||
const graph = computed(() => {
|
||||
// If we're in a subgraph, use that; otherwise use the canvas graph
|
||||
const activeSubgraph = workflowStore.activeSubgraph
|
||||
return activeSubgraph || canvas.value?.graph
|
||||
})
|
||||
|
||||
const containerStyles = computed(() => ({
|
||||
width: `${width}px`,
|
||||
@@ -627,7 +632,8 @@ export function useMinimap() {
|
||||
c.setDirty(true, true)
|
||||
}
|
||||
|
||||
let originalCallbacks: GraphCallbacks = {}
|
||||
// Map to store original callbacks per graph ID
|
||||
const originalCallbacksMap = new Map<string, GraphCallbacks>()
|
||||
|
||||
const handleGraphChanged = useThrottleFn(() => {
|
||||
needsFullRedraw.value = true
|
||||
@@ -641,11 +647,18 @@ export function useMinimap() {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
originalCallbacks = {
|
||||
// Check if we've already wrapped this graph's callbacks
|
||||
if (originalCallbacksMap.has(g.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store the original callbacks for this graph
|
||||
const originalCallbacks: GraphCallbacks = {
|
||||
onNodeAdded: g.onNodeAdded,
|
||||
onNodeRemoved: g.onNodeRemoved,
|
||||
onConnectionChange: g.onConnectionChange
|
||||
}
|
||||
originalCallbacksMap.set(g.id, originalCallbacks)
|
||||
|
||||
g.onNodeAdded = function (node) {
|
||||
originalCallbacks.onNodeAdded?.call(this, node)
|
||||
@@ -670,15 +683,18 @@ export function useMinimap() {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
if (originalCallbacks.onNodeAdded !== undefined) {
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
}
|
||||
if (originalCallbacks.onNodeRemoved !== undefined) {
|
||||
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
}
|
||||
if (originalCallbacks.onConnectionChange !== undefined) {
|
||||
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
const originalCallbacks = originalCallbacksMap.get(g.id)
|
||||
if (!originalCallbacks) {
|
||||
throw new Error(
|
||||
'Attempted to cleanup event listeners for graph that was never set up'
|
||||
)
|
||||
}
|
||||
|
||||
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||
|
||||
originalCallbacksMap.delete(g.id)
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
@@ -751,6 +767,19 @@ export function useMinimap() {
|
||||
{ immediate: true, flush: 'post' }
|
||||
)
|
||||
|
||||
// Watch for graph changes (e.g., when navigating to/from subgraphs)
|
||||
watch(graph, (newGraph, oldGraph) => {
|
||||
if (newGraph && newGraph !== oldGraph) {
|
||||
cleanupEventListeners()
|
||||
setupEventListeners()
|
||||
needsFullRedraw.value = true
|
||||
updateFlags.value.bounds = true
|
||||
updateFlags.value.nodes = true
|
||||
updateFlags.value.connections = true
|
||||
updateMinimap()
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, async (isVisible) => {
|
||||
if (isVisible) {
|
||||
if (containerRef.value) {
|
||||
|
||||
@@ -772,7 +772,8 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
{
|
||||
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
|
||||
name: 'Low quality rendering zoom threshold',
|
||||
tooltip: 'Render low quality shapes when zoomed out',
|
||||
tooltip:
|
||||
'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details.',
|
||||
type: 'slider',
|
||||
attrs: {
|
||||
min: 0.1,
|
||||
|
||||
@@ -3608,6 +3608,7 @@ export class LGraphCanvas
|
||||
subgraphs: []
|
||||
}
|
||||
|
||||
// NOTE: logic for traversing nested subgraphs depends on this being a set.
|
||||
const subgraphs = new Set<Subgraph>()
|
||||
|
||||
// Create serialisable objects
|
||||
@@ -3646,8 +3647,13 @@ export class LGraphCanvas
|
||||
}
|
||||
|
||||
// Add unique subgraph entries
|
||||
// TODO: Must find all nested subgraphs
|
||||
// NOTE: subgraphs is appended to mid iteration.
|
||||
for (const subgraph of subgraphs) {
|
||||
for (const node of subgraph.nodes) {
|
||||
if (node instanceof SubgraphNode) {
|
||||
subgraphs.add(node.subgraph)
|
||||
}
|
||||
}
|
||||
const cloned = subgraph.clone(true).asSerialisable()
|
||||
serialisable.subgraphs.push(cloned)
|
||||
}
|
||||
@@ -3764,12 +3770,19 @@ export class LGraphCanvas
|
||||
created.push(group)
|
||||
}
|
||||
|
||||
// Update subgraph ids with nesting
|
||||
function updateSubgraphIds(nodes: { type: string }[]) {
|
||||
for (const info of nodes) {
|
||||
const subgraph = results.subgraphs.get(info.type)
|
||||
if (!subgraph) continue
|
||||
info.type = subgraph.id
|
||||
updateSubgraphIds(subgraph.nodes)
|
||||
}
|
||||
}
|
||||
updateSubgraphIds(parsed.nodes)
|
||||
|
||||
// Nodes
|
||||
for (const info of parsed.nodes) {
|
||||
// If the subgraph was cloned, update references to use the new subgraph ID.
|
||||
const subgraph = results.subgraphs.get(info.type)
|
||||
if (subgraph) info.type = subgraph.id
|
||||
|
||||
const node = info.type == null ? null : LiteGraph.createNode(info.type)
|
||||
if (!node) {
|
||||
// failedNodes.push(info)
|
||||
|
||||
@@ -414,6 +414,18 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
* If `input` or `output`, reroutes will not be automatically removed, and retain a connection to the input or output, respectively.
|
||||
*/
|
||||
disconnect(network: LinkNetwork, keepReroutes?: 'input' | 'output'): void {
|
||||
// Clean up the target node's input slot
|
||||
if (this.target_id !== -1) {
|
||||
const targetNode = network.getNodeById(this.target_id)
|
||||
if (targetNode) {
|
||||
const targetInput = targetNode.inputs?.[this.target_slot]
|
||||
if (targetInput && targetInput.link === this.id) {
|
||||
targetInput.link = null
|
||||
targetNode.setDirtyCanvas?.(true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const reroutes = LLink.getReroutes(network, this)
|
||||
|
||||
const lastReroute = reroutes.at(-1)
|
||||
|
||||
@@ -16,7 +16,10 @@ import type {
|
||||
GraphOrSubgraph,
|
||||
Subgraph
|
||||
} from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { ExportedSubgraphInstance } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type {
|
||||
ExportedSubgraphInstance,
|
||||
ISerialisedNode
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { toConcreteWidget } from '@/lib/litegraph/src/widgets/widgetMap'
|
||||
@@ -540,4 +543,36 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes widget values from this SubgraphNode instance to the
|
||||
* corresponding widgets in the subgraph definition before serialization.
|
||||
* This ensures nested subgraph widget values are preserved when saving.
|
||||
*/
|
||||
override serialize(): ISerialisedNode {
|
||||
// Sync widget values to subgraph definition before serialization
|
||||
for (let i = 0; i < this.widgets.length; i++) {
|
||||
const widget = this.widgets[i]
|
||||
const input = this.inputs.find((inp) => inp.name === widget.name)
|
||||
|
||||
if (input) {
|
||||
const subgraphInput = this.subgraph.inputNode.slots.find(
|
||||
(slot) => slot.name === input.name
|
||||
)
|
||||
|
||||
if (subgraphInput) {
|
||||
// Find all widgets connected to this subgraph input
|
||||
const connectedWidgets = subgraphInput.getConnectedWidgets()
|
||||
|
||||
// Update the value of all connected widgets
|
||||
for (const connectedWidget of connectedWidgets) {
|
||||
connectedWidget.value = widget.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call parent serialize method
|
||||
return super.serialize()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraph, LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import { test } from './testExtensions'
|
||||
|
||||
@@ -14,4 +14,84 @@ describe('LLink', () => {
|
||||
const link = new LLink(1, 'float', 4, 2, 5, 3)
|
||||
expect(link.serialize()).toMatchSnapshot('Basic')
|
||||
})
|
||||
|
||||
describe('disconnect', () => {
|
||||
it('should clear the target input link reference when disconnecting', () => {
|
||||
// Create a graph and nodes
|
||||
const graph = new LGraph()
|
||||
const sourceNode = new LGraphNode('Source')
|
||||
const targetNode = new LGraphNode('Target')
|
||||
|
||||
// Add nodes to graph
|
||||
graph.add(sourceNode)
|
||||
graph.add(targetNode)
|
||||
|
||||
// Add slots
|
||||
sourceNode.addOutput('out', 'number')
|
||||
targetNode.addInput('in', 'number')
|
||||
|
||||
// Connect the nodes
|
||||
const link = sourceNode.connect(0, targetNode, 0)
|
||||
expect(link).toBeDefined()
|
||||
expect(targetNode.inputs[0].link).toBe(link?.id)
|
||||
|
||||
// Mock setDirtyCanvas
|
||||
const setDirtyCanvasSpy = vi.spyOn(targetNode, 'setDirtyCanvas')
|
||||
|
||||
// Disconnect the link
|
||||
link?.disconnect(graph)
|
||||
|
||||
// Verify the target input's link reference is cleared
|
||||
expect(targetNode.inputs[0].link).toBeNull()
|
||||
|
||||
// Verify setDirtyCanvas was called
|
||||
expect(setDirtyCanvasSpy).toHaveBeenCalledWith(true, false)
|
||||
})
|
||||
|
||||
it('should handle disconnecting when target node is not found', () => {
|
||||
// Create a link with invalid target
|
||||
const graph = new LGraph()
|
||||
const link = new LLink(1, 'number', 1, 0, 999, 0) // Invalid target id
|
||||
|
||||
// Should not throw when disconnecting
|
||||
expect(() => link.disconnect(graph)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should only clear link reference if it matches the current link id', () => {
|
||||
// Create a graph and nodes
|
||||
const graph = new LGraph()
|
||||
const sourceNode1 = new LGraphNode('Source1')
|
||||
const sourceNode2 = new LGraphNode('Source2')
|
||||
const targetNode = new LGraphNode('Target')
|
||||
|
||||
// Add nodes to graph
|
||||
graph.add(sourceNode1)
|
||||
graph.add(sourceNode2)
|
||||
graph.add(targetNode)
|
||||
|
||||
// Add slots
|
||||
sourceNode1.addOutput('out', 'number')
|
||||
sourceNode2.addOutput('out', 'number')
|
||||
targetNode.addInput('in', 'number')
|
||||
|
||||
// Create first connection
|
||||
const link1 = sourceNode1.connect(0, targetNode, 0)
|
||||
expect(link1).toBeDefined()
|
||||
|
||||
// Disconnect first connection
|
||||
targetNode.disconnectInput(0)
|
||||
|
||||
// Create second connection
|
||||
const link2 = sourceNode2.connect(0, targetNode, 0)
|
||||
expect(link2).toBeDefined()
|
||||
expect(targetNode.inputs[0].link).toBe(link2?.id)
|
||||
|
||||
// Try to disconnect the first link (which is already disconnected)
|
||||
// It should not affect the current connection
|
||||
link1?.disconnect(graph)
|
||||
|
||||
// The input should still have the second link
|
||||
expect(targetNode.inputs[0].link).toBe(link2?.id)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
},
|
||||
"LiteGraph_Canvas_LowQualityRenderingZoomThreshold": {
|
||||
"name": "Low quality rendering zoom threshold",
|
||||
"tooltip": "Render low quality shapes when zoomed out"
|
||||
"tooltip": "Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details."
|
||||
},
|
||||
"LiteGraph_Canvas_MaximumFps": {
|
||||
"name": "Maximum FPS",
|
||||
|
||||
@@ -83,8 +83,8 @@
|
||||
}
|
||||
},
|
||||
"breadcrumbsMenu": {
|
||||
"clearWorkflow": "清空工作流",
|
||||
"deleteWorkflow": "删除工作流",
|
||||
"clearWorkflow": "清除工作流程",
|
||||
"deleteWorkflow": "删除工作流程",
|
||||
"duplicate": "复制",
|
||||
"enterNewName": "输入新名称"
|
||||
},
|
||||
@@ -218,7 +218,7 @@
|
||||
"WEBCAM": "摄像头"
|
||||
},
|
||||
"desktopMenu": {
|
||||
"confirmQuit": "存在未保存的工作流;任何未保存的更改都将丢失。忽略此警告并退出?",
|
||||
"confirmQuit": "有未保存的工作流程开启;任何未保存的更改都将丢失。忽略此警告并退出?",
|
||||
"confirmReinstall": "这将清除您的 extra_models_config.yaml 文件,并重新开始安装。您确定吗?",
|
||||
"quit": "退出",
|
||||
"reinstall": "重新安装"
|
||||
@@ -313,7 +313,7 @@
|
||||
"filter": "过滤",
|
||||
"findIssues": "查找问题",
|
||||
"firstTimeUIMessage": "这是您第一次使用新界面。选择 \"菜单 > 使用新菜单 > 禁用\" 来恢复旧界面。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能与后端版本 {backendVersion} 不兼容。",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。后端需要 {requiredVersion} 或更高版本。",
|
||||
"goToNode": "转到节点",
|
||||
"help": "帮助",
|
||||
@@ -400,8 +400,8 @@
|
||||
"upload": "上传",
|
||||
"usageHint": "使用提示",
|
||||
"user": "用户",
|
||||
"versionMismatchWarning": "版本兼容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 请参考 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新说明。",
|
||||
"versionMismatchWarning": "版本相容性警告",
|
||||
"versionMismatchWarningMessage": "{warning}:{detail} 请参阅 https://docs.comfy.org/installation/update_comfyui#common-update-issues 以取得更新说明。",
|
||||
"videoFailedToLoad": "视频加载失败",
|
||||
"workflow": "工作流"
|
||||
},
|
||||
@@ -594,7 +594,7 @@
|
||||
"wireframe": "线框"
|
||||
},
|
||||
"model": "模型",
|
||||
"openIn3DViewer": "在 3D 浏览器中打开",
|
||||
"openIn3DViewer": "在 3D 查看器中打开",
|
||||
"previewOutput": "预览输出",
|
||||
"removeBackgroundImage": "移除背景图片",
|
||||
"resizeNodeMatchOutput": "调整节点以匹配输出",
|
||||
@@ -611,7 +611,7 @@
|
||||
"uploadBackgroundImage": "上传背景图片",
|
||||
"uploadTexture": "上传纹理",
|
||||
"viewer": {
|
||||
"apply": "套用",
|
||||
"apply": "应用",
|
||||
"cameraSettings": "相机设置",
|
||||
"cameraType": "相机类型",
|
||||
"cancel": "取消",
|
||||
@@ -619,7 +619,7 @@
|
||||
"lightSettings": "灯光设置",
|
||||
"modelSettings": "模型设置",
|
||||
"sceneSettings": "场景设置",
|
||||
"title": "3D 浏览器(测试版)"
|
||||
"title": "3D 查看器(测试版)"
|
||||
}
|
||||
},
|
||||
"loadWorkflowWarning": {
|
||||
@@ -740,24 +740,24 @@
|
||||
"disabled": "禁用",
|
||||
"disabledTooltip": "工作流将不会自动执行",
|
||||
"execute": "执行",
|
||||
"help": "帮助",
|
||||
"help": "说明",
|
||||
"hideMenu": "隐藏菜单",
|
||||
"instant": "实时",
|
||||
"instantTooltip": "工作流将会在生成完成后立即执行",
|
||||
"interrupt": "取消当前任务",
|
||||
"light": "淺色",
|
||||
"manageExtensions": "管理扩展功能",
|
||||
"manageExtensions": "管理擴充功能",
|
||||
"onChange": "更改时",
|
||||
"onChangeTooltip": "一旦进行更改,工作流将添加到执行队列",
|
||||
"queue": "队列面板",
|
||||
"refresh": "刷新节点",
|
||||
"resetView": "重置视图",
|
||||
"run": "运行",
|
||||
"runWorkflow": "运行工作流(Shift插队)",
|
||||
"runWorkflowFront": "运行工作流(插队)",
|
||||
"settings": "设置",
|
||||
"runWorkflow": "运行工作流程(Shift排在前面)",
|
||||
"runWorkflowFront": "运行工作流程(排在前面)",
|
||||
"settings": "设定",
|
||||
"showMenu": "显示菜单",
|
||||
"theme": "主題",
|
||||
"theme": "主题",
|
||||
"toggleBottomPanel": "底部面板"
|
||||
},
|
||||
"menuLabels": {
|
||||
@@ -786,7 +786,7 @@
|
||||
"Desktop User Guide": "桌面端用户指南",
|
||||
"Duplicate Current Workflow": "复制当前工作流",
|
||||
"Edit": "编辑",
|
||||
"Exit Subgraph": "退出子圖",
|
||||
"Exit Subgraph": "退出子图",
|
||||
"Export": "导出",
|
||||
"Export (API)": "导出 (API)",
|
||||
"File": "文件",
|
||||
@@ -801,7 +801,7 @@
|
||||
"Load Default Workflow": "加载默认工作流",
|
||||
"Manage group nodes": "管理组节点",
|
||||
"Manager": "管理器",
|
||||
"Minimap": "缩略地图",
|
||||
"Minimap": "小地图",
|
||||
"Model Library": "模型库",
|
||||
"Move Selected Nodes Down": "下移所选节点",
|
||||
"Move Selected Nodes Left": "左移所选节点",
|
||||
@@ -811,9 +811,9 @@
|
||||
"New": "新建",
|
||||
"Next Opened Workflow": "下一个打开的工作流",
|
||||
"Node Library": "节点库",
|
||||
"Node Links": "节点连线",
|
||||
"Node Links": "节点连接",
|
||||
"Open": "打开",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "为所选节点开启 3D 浏览器(Beta 版)",
|
||||
"Open 3D Viewer (Beta) for Selected Node": "为选中节点打开3D查看器(测试版)",
|
||||
"Open Custom Nodes Folder": "打开自定义节点文件夹",
|
||||
"Open DevTools": "打开开发者工具",
|
||||
"Open Inputs Folder": "打开输入文件夹",
|
||||
@@ -842,28 +842,28 @@
|
||||
"Show Keybindings Dialog": "显示快捷键对话框",
|
||||
"Show Settings Dialog": "显示设置对话框",
|
||||
"Sign Out": "退出登录",
|
||||
"Toggle Essential Bottom Panel": "切换基本下方面板",
|
||||
"Toggle Essential Bottom Panel": "切换基础底部面板",
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
"Toggle Theme (Dark/Light)": "切换主题(暗/亮)",
|
||||
"Toggle View Controls Bottom Panel": "切换视图控制下方面板",
|
||||
"Toggle View Controls Bottom Panel": "切换视图控制底部面板",
|
||||
"Toggle the Custom Nodes Manager": "切换自定义节点管理器",
|
||||
"Toggle the Custom Nodes Manager Progress Bar": "切换自定义节点管理器进度条",
|
||||
"Undo": "撤销",
|
||||
"Ungroup selected group nodes": "解散选中组节点",
|
||||
"Unpack the selected Subgraph": "解開所選子圖",
|
||||
"Workflows": "工作流程",
|
||||
"Unpack the selected Subgraph": "解包选中子图",
|
||||
"Workflows": "工作流",
|
||||
"Zoom In": "放大画面",
|
||||
"Zoom Out": "缩小画面",
|
||||
"Zoom to fit": "缩放至适合大小"
|
||||
"Zoom to fit": "缩放以适应"
|
||||
},
|
||||
"minimap": {
|
||||
"nodeColors": "节点颜色",
|
||||
"renderBypassState": "显示绕过状态",
|
||||
"renderErrorState": "显示错误状态",
|
||||
"showGroups": "显示框架/群组",
|
||||
"showLinks": "显示连线"
|
||||
"renderBypassState": "渲染绕过状态",
|
||||
"renderErrorState": "渲染错误状态",
|
||||
"showGroups": "显示框架/分组",
|
||||
"showLinks": "显示连接"
|
||||
},
|
||||
"missingModelsDialog": {
|
||||
"doNotAskAgain": "不再显示此消息",
|
||||
@@ -1131,7 +1131,7 @@
|
||||
},
|
||||
"settingsCategories": {
|
||||
"3D": "3D",
|
||||
"3DViewer": "3D 浏览器",
|
||||
"3DViewer": "3D查看器",
|
||||
"API Nodes": "API 节点",
|
||||
"About": "关于",
|
||||
"Appearance": "外观",
|
||||
@@ -1184,7 +1184,7 @@
|
||||
"Workflow": "工作流"
|
||||
},
|
||||
"shortcuts": {
|
||||
"essentials": "基本功能",
|
||||
"essentials": "常用",
|
||||
"keyboardShortcuts": "键盘快捷键",
|
||||
"manageShortcuts": "管理快捷键",
|
||||
"noKeybinding": "无快捷键",
|
||||
@@ -1616,7 +1616,7 @@
|
||||
"failedToExportModel": "无法将模型导出为 {format}",
|
||||
"failedToFetchBalance": "获取余额失败:{error}",
|
||||
"failedToFetchLogs": "无法获取服务器日志",
|
||||
"failedToInitializeLoad3dViewer": "初始化 3D 浏览器失败",
|
||||
"failedToInitializeLoad3dViewer": "初始化3D查看器失败",
|
||||
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
|
||||
"failedToPurchaseCredits": "购买积分失败:{error}",
|
||||
"fileLoadError": "无法在 {fileName} 中找到工作流",
|
||||
@@ -1675,7 +1675,7 @@
|
||||
"versionMismatchWarning": {
|
||||
"dismiss": "关闭",
|
||||
"frontendNewer": "前端版本 {frontendVersion} 可能與後端版本 {backendVersion} 不相容。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已過時。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"frontendOutdated": "前端版本 {frontendVersion} 已过时。後端需要 {requiredVersion} 版或更高版本。",
|
||||
"title": "版本相容性警告",
|
||||
"updateFrontend": "更新前端"
|
||||
},
|
||||
|
||||
@@ -120,8 +120,8 @@
|
||||
}
|
||||
},
|
||||
"Comfy_Load3D_3DViewerEnable": {
|
||||
"name": "启用 3D 浏览器(测试版)",
|
||||
"tooltip": "为所选节点启用 3D 浏览器(测试版)。此功能可让您直接在全尺寸 3D 浏览器中浏览并与 3D 模型交互。"
|
||||
"name": "启用3D查看器(测试版)",
|
||||
"tooltip": "为选定节点启用3D查看器(测试版)。此功能允许你在全尺寸3D查看器中直接可视化和交互3D模型。"
|
||||
},
|
||||
"Comfy_Load3D_BackgroundColor": {
|
||||
"name": "初始背景颜色",
|
||||
@@ -338,7 +338,7 @@
|
||||
"Disabled": "禁用",
|
||||
"Top": "顶部"
|
||||
},
|
||||
"tooltip": "菜单列位置。在移动设备上,菜单始终显示于顶端。"
|
||||
"tooltip": "选单列位置。在行动装置上,选单始终显示于顶端。"
|
||||
},
|
||||
"Comfy_Validation_Workflows": {
|
||||
"name": "校验工作流"
|
||||
|
||||
@@ -11,7 +11,8 @@ const mockT = vi.fn((key: string) => {
|
||||
'shortcuts.subcategories.node': 'Node',
|
||||
'shortcuts.subcategories.queue': 'Queue',
|
||||
'shortcuts.subcategories.view': 'View',
|
||||
'shortcuts.subcategories.panelControls': 'Panel Controls'
|
||||
'shortcuts.subcategories.panelControls': 'Panel Controls',
|
||||
'commands.Workflow_New.label': 'New Blank Workflow'
|
||||
}
|
||||
return translations[key] || key
|
||||
})
|
||||
@@ -76,9 +77,7 @@ describe('ShortcutsList', () => {
|
||||
expect(wrapper.text()).toContain('Queue')
|
||||
|
||||
// Check that commands are rendered
|
||||
expect(wrapper.text()).toContain('New Workflow')
|
||||
expect(wrapper.text()).toContain('Add Node')
|
||||
expect(wrapper.text()).toContain('Clear Queue')
|
||||
expect(wrapper.text()).toContain('New Blank Workflow')
|
||||
})
|
||||
|
||||
it('should format keyboard shortcuts correctly', () => {
|
||||
|
||||
@@ -1512,7 +1512,10 @@ describe('useNodePricing', () => {
|
||||
{ model: 'gpt-4o', expected: '$0.0025/$0.01 per 1K tokens' },
|
||||
{ model: 'gpt-4.1-nano', expected: '$0.0001/$0.0004 per 1K tokens' },
|
||||
{ model: 'gpt-4.1-mini', expected: '$0.0004/$0.0016 per 1K tokens' },
|
||||
{ model: 'gpt-4.1', expected: '$0.002/$0.008 per 1K tokens' }
|
||||
{ model: 'gpt-4.1', expected: '$0.002/$0.008 per 1K tokens' },
|
||||
{ model: 'gpt-5-nano', expected: '$0.00005/$0.0004 per 1K tokens' },
|
||||
{ model: 'gpt-5-mini', expected: '$0.00025/$0.002 per 1K tokens' },
|
||||
{ model: 'gpt-5', expected: '$0.00125/$0.01 per 1K tokens' }
|
||||
]
|
||||
|
||||
testCases.forEach(({ model, expected }) => {
|
||||
|
||||
38
tests-ui/tests/issue-5033-type-compatibility.test.ts
Normal file
38
tests-ui/tests/issue-5033-type-compatibility.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Test for issue #5033: Type compatibility between comfyui-frontend-types and @comfyorg/litegraph
|
||||
*
|
||||
* This test verifies that the generated types from this package can be used alongside
|
||||
* external litegraph types without causing "#private field" conflicts.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { LGraph, LLink } from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
describe('Issue #5033: Type compatibility', () => {
|
||||
it('should allow ComfyApp.graph to be assigned to LGraph type', () => {
|
||||
// This test verifies that the types are compatible after removing #private fields
|
||||
// from the generated .d.ts files
|
||||
|
||||
function getGraph(app: ComfyApp): LGraph {
|
||||
// This should not cause TypeScript errors about #private field conflicts
|
||||
return app.graph
|
||||
}
|
||||
|
||||
// Type test - if this compiles, the issue is fixed
|
||||
expect(typeof getGraph).toBe('function')
|
||||
})
|
||||
|
||||
it('should allow graph.links to be compatible with LLink type', () => {
|
||||
type LGraphFromApp = ComfyApp['graph']
|
||||
|
||||
function getLinks(app: ComfyApp): LLink | null {
|
||||
const graph: LGraphFromApp = app.graph
|
||||
// This should not cause TypeScript errors about #private field conflicts
|
||||
return graph.links.get(0) ?? null
|
||||
}
|
||||
|
||||
// Type test - if this compiles, the issue is fixed
|
||||
expect(typeof getLinks).toBe('function')
|
||||
})
|
||||
})
|
||||
@@ -18,7 +18,18 @@ export default defineConfig({
|
||||
dts({
|
||||
copyDtsFiles: true,
|
||||
rollupTypes: true,
|
||||
tsconfigPath: 'tsconfig.types.json'
|
||||
tsconfigPath: 'tsconfig.types.json',
|
||||
beforeWriteFile: (filePath, content) => {
|
||||
// Remove #private field declarations to prevent conflicts with external packages
|
||||
// This fixes issue #5033 where #private fields cause type incompatibility
|
||||
const cleanedContent = content
|
||||
.replace(/\s*#private;\s*/g, '') // Remove "#private;" declarations
|
||||
.replace(/\s*#[a-zA-Z_$][a-zA-Z0-9_$]*\s*:\s*[^;]+;\s*/g, '') // Remove full private field declarations
|
||||
return {
|
||||
filePath,
|
||||
content: cleanedContent
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user