Compare commits

...

11 Commits

Author SHA1 Message Date
snomiao
11098f7f9b [bugfix] Fix #private field conflicts in generated types
Remove #private field declarations from generated .d.ts files to prevent
TypeScript compatibility issues when using @comfyorg/comfyui-frontend-types
alongside @comfyorg/litegraph packages.

Fixes #5033

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-17 09:54:28 +00:00
Comfy Org PR Bot
65785af348 [release] Increment version to 1.26.4 (#5032)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-08-15 20:21:20 -07:00
Arjan Singh
ec4ad5ea92 fix: issue #4121 (#5029) 2025-08-15 18:41:14 -07:00
Christian Byrne
e9ddf29507 [bugfix] Preserve nested subgraph widget values during serialization (#5023)
When saving workflows with nested subgraphs, promoted widget values were not being synchronized back to the subgraph definitions before serialization. This caused widget values to revert to their original defaults when reloading the workflow.

The fix overrides the serialize() method in SubgraphNode to sync promoted widget values to their corresponding widgets in the subgraph definition before serialization occurs.

Fixes the issue where nested subgraph widget values would be lost after save/reload.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-15 14:35:11 -07:00
AustinMroz
fdd8564c07 Deep copy subgraphs to clipboard, update nested ids on paste (#5003)
* Deep copy to clipboard, update nested ids on paste

The copyToClipboard function wasn't walking subgraphs and leaving nested
subgraphs unserialized. This has now been fixed.

This requires that equivalent support be added to _pasteFromClipboard to
update the ids of nested subgraphs which are pasted.

* Add extra advisory comments
2025-08-15 14:03:29 -07:00
Christian Byrne
d18081a54e fix: improve minimap subgraph navigation with graph UUID callback tracking (#5018)
- Replace single callback storage with Map using graph UUIDs as keys
- Fix minimap not updating when navigating between subgraphs
- Add proper cleanup and error handling for callback management
- Switch from app.canvas.graph to reactive workflowStore.activeSubgraph
- Prevent callback wrapping recursion by tracking setup state per graph
2025-08-15 13:34:44 -07:00
Christian Byrne
45cc6ca2b4 Fix widget disconnection issue in subgraphs #4922 (#5015)
* [bugfix] Fix widget disconnection issue in subgraphs

When disconnecting a node from a SubgraphInput, the target input's link
reference was not being cleared in LLink.disconnect(). This caused
widgets to remain greyed out because they still thought they were
connected (slot.link was not null).

The fix ensures that when a link is disconnected, the target node's
input slot is properly cleaned up by setting input.link = null.

Fixes #4922

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [test] Add tests for LLink disconnect fix for widget issue

Add comprehensive tests for the LLink.disconnect() method to verify
that target input link references are properly cleared when disconnecting.
This prevents widgets from remaining greyed out after disconnection.

Tests cover:
- Basic disconnect functionality with link reference cleanup
- Edge cases with invalid target nodes
- Preventing interference between different connections

Related to #4922

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-15 13:12:47 -07:00
Christian Byrne
c303a3f037 [fix] Complete traditional to simplified Chinese character conversion (#5013)
* [fix] Complete traditional to simplified Chinese character conversion

Fixes issue where the automated translation system was incorrectly
mixing traditional Chinese characters into simplified Chinese (zh)
locale files after PR #4410 added zh-TW support.

Changes:
- Updated .i18nrc.cjs with explicit guidelines for AI model to
  distinguish between simplified and traditional Chinese
- Fixed 50+ traditional characters in zh locale files:
  - commands.json: 畫→画, 減→减, 筆→笔
  - main.json: 關→关, 刪→删, 複→复, 製→制, 輸→输, etc.
  - settings.json: 舊→旧, 標→标, 選→选, etc.

Completed the systematic conversion work started in PRs #5005 and #4865
without overwriting any human translator decisions.

Fixes #5010

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update locales [skip ci]

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-08-15 13:01:05 -07:00
Alexander Piskun
c90fd18ade api_nodes: added prices for gpt-5 series models (#4958) 2025-08-15 12:36:18 -07:00
Johnpaul Chiwetelu
2ed1704749 Translated Keyboard Shortcuts (#5007)
* fix: Update command label rendering to use i18n normalization

* fix: Replace deprecated  with t for command label rendering

* fix: Simplify command rendering check in ShortcutsList tests

* fix: Add missing translation for command label in ShortcutsList tests
2025-08-15 11:45:10 -07:00
Christian Byrne
7d5a4d423e [feat] Improve low quality rendering zoom threshold tooltip (#5009)
* [docs] Improve low quality rendering zoom threshold tooltip

Clarify the behavior of the setting to explain that lower values maintain quality when zoomed out, while higher values enable simplified rendering at normal zoom levels.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update locales [skip ci]

* [docs] Improve low quality rendering zoom threshold tooltip

Clarify the behavior of the setting to explain that lower values maintain quality when zoomed out, while higher values enable simplified rendering at normal zoom levels.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update locales [skip ci]

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-08-15 11:44:26 -07:00
19 changed files with 304 additions and 74 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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()

View File

@@ -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

View File

@@ -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'
}

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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)
})
})
})

View File

@@ -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",

View File

@@ -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": "更新前端"
},

View File

@@ -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": "校验工作流"

View File

@@ -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', () => {

View File

@@ -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 }) => {

View 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')
})
})

View File

@@ -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
}
}
})
]
})