Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ CAP_SYSTEM_VERSION ?= 2.0.3
# 避免内置依赖指纹变化触发存量 ComfyUI 冷启动时重跑 install_all
BUILTIN_DEPENDENCY_VERSION ?= $(CAP_SYSTEM_VERSION)
else
CAP_SYSTEM_VERSION ?= 1.6.8
CAP_SYSTEM_VERSION ?= 1.6.7
BUILTIN_DEPENDENCY_VERSION ?= $(CAP_SYSTEM_VERSION)
endif
AGENT_COMFYUI_IMAGE ?= cap-demo-public-registry.cn-hangzhou.cr.aliyuncs.com/aliyunfc/funart-comfyui:v$(CAP_SYSTEM_VERSION)
Expand Down
17 changes: 15 additions & 2 deletions src/code/agent/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ def _read_comfyui_version() -> str:
'--multi-user', # 与 FunArt-ComfyUI-Multi-User 多租 patch 冲突
}

# CPU 模式允许消费的「CPU 安全」自定义启动参数白名单
# 仅与 GPU 无关的纯功能参数;GPU 专属参数(如 --highvram)不在此列,避免 CPU 启动失败
# value 声明该参数是否接收一个值(arity):--disable-api-nodes 是布尔开关,不接值
_CPU_SAFE_BOOT_ARGS = {
'--disable-api-nodes': False, # 禁用 ComfyUI 官方 API 节点(Comfy Core api 节点),布尔开关
}

# 根据 COMFYUI_MODE 构建启动命令
if COMFYUI_MODE == 'cpu':
# CPU模式:输入目录默认使用 MNT_INPUT_DIR
Expand Down Expand Up @@ -122,15 +129,21 @@ def _read_comfyui_version() -> str:
]

# 构建最终的启动命令
# CPU 模式不使用 CUSTOM_BOOT_ARGS,GPU 模式才使用
# GPU 模式消费全部非受保护的 CUSTOM_BOOT_ARGS;
# CPU 模式只放行「CPU 安全」白名单参数,避免 GPU 专属参数导致 ComfyUI 启动失败
if COMFYUI_MODE == 'gpu':
COMFYUI_BOOT_CMD = build_boot_command(
base_cmd=_base_boot_cmd,
custom_boot_args=CUSTOM_BOOT_ARGS,
protected_args=_PROTECTED_ARGS
)
else:
COMFYUI_BOOT_CMD = _base_boot_cmd
COMFYUI_BOOT_CMD = build_boot_command(
base_cmd=_base_boot_cmd,
custom_boot_args=CUSTOM_BOOT_ARGS,
protected_args=_PROTECTED_ARGS,
allowed_args=_CPU_SAFE_BOOT_ARGS
)

SD_DIR = os.getenv('SD_DIR', WORK_DIR + '/stable-diffusion-webui')
SD_PROCESS_PORT = 7860
Expand Down
106 changes: 106 additions & 0 deletions src/code/agent/test/unit/utils/args_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from utils.args import (
parse_extra_boot_args,
filter_protected_args,
filter_allowed_args,
build_boot_command,
)

Expand Down Expand Up @@ -304,3 +305,108 @@ def test_complete_workflow_real_scenario(self):
'--use-pytorch-cross-attention'
]
assert result == expected


class TestFilterAllowedArgs:
"""测试 filter_allowed_args 白名单过滤(CPU 模式,allowed_specs 带 arity)"""

def test_keep_flag(self):
valid, excluded = filter_allowed_args(['--disable-api-nodes'], {'--disable-api-nodes': False})
assert valid == ['--disable-api-nodes']
assert excluded == []

def test_drop_non_allowlisted_arg(self):
valid, excluded = filter_allowed_args(['--highvram'], {'--disable-api-nodes': False})
assert valid == []
assert excluded == ['--highvram']

def test_mixed_keep_only_allowlisted(self):
valid, excluded = filter_allowed_args(
['--disable-api-nodes', '--highvram'], {'--disable-api-nodes': False})
assert valid == ['--disable-api-nodes']
assert excluded == ['--highvram']

def test_flag_drops_trailing_value(self):
# 布尔开关后面的 token 不应被当成值保留(否则给 store_true 传值会让 argparse 启动失败)
valid, excluded = filter_allowed_args(
['--disable-api-nodes', 'true'], {'--disable-api-nodes': False})
assert valid == ['--disable-api-nodes']
assert excluded == []

def test_flag_strips_eq_value(self):
# --key=value 对布尔开关规整为纯 flag
valid, excluded = filter_allowed_args(
['--disable-api-nodes=1'], {'--disable-api-nodes': False})
assert valid == ['--disable-api-nodes']
assert excluded == []

def test_drop_non_allowlisted_arg_with_value(self):
# 非白名单参数及其值都应被丢弃
valid, excluded = filter_allowed_args(
['--preview-method', 'auto'], {'--disable-api-nodes': False})
assert valid == []
assert excluded == ['--preview-method']

def test_value_taking_arg_keeps_value(self):
# arity=True 的白名单参数保留其后续值
valid, excluded = filter_allowed_args(['--foo', 'bar'], {'--foo': True})
assert valid == ['--foo', 'bar']
assert excluded == []

def test_value_taking_arg_keeps_eq_form(self):
valid, excluded = filter_allowed_args(['--foo=bar'], {'--foo': True})
assert valid == ['--foo=bar']
assert excluded == []

def test_empty(self):
valid, excluded = filter_allowed_args([], {'--disable-api-nodes': False})
assert valid == []
assert excluded == []


class TestBuildBootCommandCpuAllowlist:
"""测试 build_boot_command 的 CPU 白名单模式(allowed_args 带 arity)"""

CPU_SAFE = {'--disable-api-nodes': False}

def test_cpu_allowlist_keeps_safe_arg(self):
base = ['python', 'main.py', '--cpu']
protected = {'--cpu', '--listen'}
result = build_boot_command(base, '--disable-api-nodes', protected, allowed_args=self.CPU_SAFE)
assert result == base + ['--disable-api-nodes']

def test_cpu_allowlist_drops_gpu_arg(self):
# GPU 专属参数在 CPU 白名单模式下被丢弃,避免启动失败
base = ['python', 'main.py', '--cpu']
protected = {'--cpu', '--listen'}
result = build_boot_command(base, '--highvram', protected, allowed_args=self.CPU_SAFE)
assert result == base

def test_cpu_allowlist_flag_drops_trailing_value(self):
# --disable-api-nodes true → 只保留 flag,避免 argparse 启动失败(P2 修复)
base = ['python', 'main.py', '--cpu']
protected = {'--cpu', '--listen'}
result = build_boot_command(base, '--disable-api-nodes true', protected, allowed_args=self.CPU_SAFE)
assert result == base + ['--disable-api-nodes']

def test_cpu_allowlist_flag_strips_eq_value(self):
base = ['python', 'main.py', '--cpu']
protected = {'--cpu', '--listen'}
result = build_boot_command(base, '--disable-api-nodes=1', protected, allowed_args=self.CPU_SAFE)
assert result == base + ['--disable-api-nodes']

def test_cpu_allowlist_mixed(self):
base = ['python', 'main.py', '--cpu']
protected = {'--cpu', '--listen'}
result = build_boot_command(
base, '--disable-api-nodes --highvram --preview-method auto',
protected, allowed_args=self.CPU_SAFE)
assert result == base + ['--disable-api-nodes']

def test_gpu_path_unchanged_when_allowed_none(self):
# allowed_args=None(GPU 路径)与不传时行为完全一致
base = ['python', 'main.py']
protected = {'--listen'}
custom = '--highvram --preview-method auto'
assert build_boot_command(base, custom, protected, allowed_args=None) == \
build_boot_command(base, custom, protected)
79 changes: 68 additions & 11 deletions src/code/agent/utils/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
参数解析工具函数
"""
import shlex
from typing import Tuple, List, Set
from typing import Tuple, List, Set, Dict

from utils.logger import log

Expand Down Expand Up @@ -75,35 +75,92 @@ def filter_protected_args(parsed_args: List[str], protected_args: Set[str]) -> T
return valid_args, excluded_args


def filter_allowed_args(parsed_args: List[str], allowed_specs: Dict[str, bool]) -> Tuple[List[str], List[str]]:
"""
白名单过滤:仅保留 allowed_specs 中的参数,其余排除

用于 CPU 模式:只放行「CPU 安全」参数,丢弃 GPU 专属参数,避免 ComfyUI 启动失败。
allowed_specs 的 value 声明该参数是否接收一个值(arity):
- False:布尔开关,不保留其后的值 token,并把 `--key=value` 规整为纯 `--key`
(避免给 store_true 开关传值导致 argparse 启动失败)
- True:接收一个值,保留紧随其后的值 token,或保留 `--key=value` 整体

Args:
parsed_args: 已解析的参数列表
allowed_specs: 白名单规格 {参数名: 是否接值}

Returns:
Tuple[List[str], List[str]]: (放行的参数列表, 被排除的参数名列表)
"""
valid_args = []
excluded_args = []

i = 0
while i < len(parsed_args):
arg = parsed_args[i]

if arg.startswith('--'):
arg_name = arg.split('=')[0]
if arg_name in allowed_specs:
if '=' in arg:
# --key=value 形式:接值则保留整体,布尔开关则规整为纯 flag
valid_args.append(arg if allowed_specs[arg_name] else arg_name)
else:
valid_args.append(arg)
else:
excluded_args.append(arg_name)
i += 1
else:
# 值 token:仅当前一个参数在白名单且声明接值(arity=True)时保留
if i > 0:
prev = parsed_args[i - 1]
prev_name = prev.split('=')[0] if prev.startswith('--') else None
if prev_name and allowed_specs.get(prev_name):
valid_args.append(arg)
i += 1

return valid_args, excluded_args


def build_boot_command(
base_cmd: List[str],
custom_boot_args: str,
protected_args: Set[str]
protected_args: Set[str],
allowed_args: Dict[str, bool] = None
) -> List[str]:
"""
构建最终的启动命令

流程:
1. 解析用户的自定义启动参数
2. 过滤掉与系统默认参数冲突的受保护参数
3. 将允许的自定义参数追加到默认命令后面

2. 若提供 allowed_args(CPU 白名单模式),先收窄到白名单内参数
3. 过滤掉与系统默认参数冲突的受保护参数
4. 将允许的自定义参数追加到默认命令后面

Args:
base_cmd: 基础启动命令列表(系统默认参数,全部受保护)
custom_boot_args: 自定义启动参数字符串
protected_args: 受保护的参数名集合(不允许用户修改)

allowed_args: 可选白名单规格 {参数名: 是否接值};非 None 时仅放行白名单内参数(CPU 模式用)。
默认 None 表示不启用白名单,GPU 模式行为不变

Returns:
List[str]: 最终的启动命令 = 默认命令 + 允许的自定义参数
"""
# 1. 解析自定义启动参数
parsed_args = parse_extra_boot_args(custom_boot_args)

# 2. 过滤受保护的参数

# 2. CPU 白名单收窄(仅当传入 allowed_args 时;GPU 调用不传,跳过此步)
if allowed_args is not None:
parsed_args, denied_args = filter_allowed_args(parsed_args, allowed_args)
if denied_args:
log("WARNING", f"[CPU_SAFE_BOOT_ARGS] Dropped non-allowlisted arguments in CPU mode: {denied_args}. Only {sorted(allowed_args)} are allowed")

# 3. 过滤受保护的参数
valid_args, excluded_args = filter_protected_args(parsed_args, protected_args)
if excluded_args:
log("WARNING", f"[CUSTOM_BOOT_ARGS] Provided arguments: {parsed_args}, excluded protected arguments: {excluded_args}, valid arguments: {valid_args}. Protected arguments cannot be overridden")
# 3. 追加允许的自定义参数到默认命令

# 4. 追加允许的自定义参数到默认命令
return base_cmd + valid_args