Files
agent-aide/aide-program/docs/04-aide-config设计.md
2025-12-13 04:37:41 +08:00

16 KiB
Raw Blame History

aide config 设计

一、命令概述

1.1 功能定位

aide config 命令用于读取和修改项目配置文件,提供命令行方式访问配置。

1.2 执行时机

  • 用户需要查看配置值时
  • 用户需要修改配置值时
  • Commands 中需要获取配置时(如 prep/exec 的默认文档路径)

1.3 命令格式

aide config get <key>
aide config set <key> <value>

参数

  • <key>:配置键,支持点号分隔(如 task.source
  • <value>:配置值(仅 set 命令需要)

二、功能需求

2.1 aide config get

功能:获取配置值

输出格式

单个值:

task.source = "task-now.md"

数组值:

flow.phases = ["flow-design", "impl", "verify", "docs", "finish"]

布尔值:

env.tools.git = true

错误处理

配置键不存在:

✗ 配置键不存在: invalid.key
  可用的配置键: task.source, task.spec, env.python.version

配置文件不存在:

✗ 配置文件不存在
  位置: .aide/config.toml
  建议: 运行 'aide init' 创建配置文件

2.2 aide config set

功能:设置配置值

输出

成功时无输出(静默原则)

错误处理

配置键不存在:

✗ 配置键不存在: invalid.key
  可用的配置键: task.source, task.spec, env.python.version

配置值类型错误:

✗ 配置值类型错误
  键: env.tools.git
  期望类型: boolean
  实际值: "yes"
  建议: 使用 true 或 false

三、实现设计

3.1 函数接口

def cmd_config(args: list[str]) -> int:
    """aide config 命令处理

    Args:
        args: 命令参数

    Returns:
        退出码0 表示成功)
    """
    pass

def config_get(key: str) -> int:
    """获取配置值

    Args:
        key: 配置键

    Returns:
        退出码
    """
    pass

def config_set(key: str, value: str) -> int:
    """设置配置值

    Args:
        key: 配置键
        value: 配置值(字符串形式)

    Returns:
        退出码
    """
    pass

3.2 实现流程

3.2.1 cmd_config

from core.output import err

def cmd_config(args: list[str]) -> int:
    """aide config 命令处理"""

    if not args:
        err(
            "缺少子命令",
            [
                "用法: aide config get <key>",
                "      aide config set <key> <value>"
            ]
        )
        return 2

    subcommand = args[0]

    if subcommand == "get":
        if len(args) < 2:
            err("缺少参数: key")
            return 2
        return config_get(args[1])

    elif subcommand == "set":
        if len(args) < 3:
            err("缺少参数: key 或 value")
            return 2
        return config_set(args[1], args[2])

    else:
        err(
            f"未知子命令: {subcommand}",
            ["可用子命令: get, set"]
        )
        return 2

3.2.2 config_get

from pathlib import Path
from core.config import Config
from core.output import err

def config_get(key: str) -> int:
    """获取配置值"""

    try:
        # 加载配置
        config = Config(Path.cwd())
        config.load()

        # 获取值
        value = config.get(key)

        if value is None:
            # 配置键不存在
            available_keys = get_available_keys(config._config)
            err(
                f"配置键不存在: {key}",
                [f"可用的配置键: {', '.join(available_keys)}"]
            )
            return 4

        # 格式化输出
        formatted_value = format_config_value(value)
        print(f"{key} = {formatted_value}")

        return 0

    except FileNotFoundError:
        err(
            "配置文件不存在",
            [
                "位置: .aide/config.toml",
                "建议: 运行 'aide init' 创建配置文件"
            ]
        )
        return 4

    except Exception as e:
        err(
            "配置读取失败",
            [f"原因: {str(e)}"]
        )
        return 4

3.2.3 config_set

def config_set(key: str, value_str: str) -> int:
    """设置配置值"""

    try:
        # 加载配置
        config = Config(Path.cwd())
        config.load()

        # 验证配置键是否存在
        if not is_valid_config_key(key):
            available_keys = get_available_keys(config._config)
            err(
                f"配置键不存在: {key}",
                [f"可用的配置键: {', '.join(available_keys)}"]
            )
            return 4

        # 解析值
        try:
            value = parse_config_value(key, value_str, config._config)
        except ValueError as e:
            err(
                "配置值类型错误",
                [
                    f"键: {key}",
                    f"原因: {str(e)}"
                ]
            )
            return 4

        # 设置值
        config.set(key, value)

        # 成功时无输出(静默原则)
        return 0

    except FileNotFoundError:
        err(
            "配置文件不存在",
            [
                "位置: .aide/config.toml",
                "建议: 运行 'aide init' 创建配置文件"
            ]
        )
        return 4

    except Exception as e:
        err(
            "配置写入失败",
            [f"原因: {str(e)}"]
        )
        return 4

3.3 辅助函数

3.3.1 格式化配置值

import json

def format_config_value(value) -> str:
    """格式化配置值用于输出

    Args:
        value: 配置值

    Returns:
        格式化后的字符串
    """
    if isinstance(value, str):
        return f'"{value}"'
    elif isinstance(value, bool):
        return "true" if value else "false"
    elif isinstance(value, (list, dict)):
        return json.dumps(value, ensure_ascii=False)
    else:
        return str(value)

3.3.2 解析配置值

def parse_config_value(key: str, value_str: str, config: dict):
    """解析配置值

    Args:
        key: 配置键
        value_str: 值字符串
        config: 当前配置(用于推断类型)

    Returns:
        解析后的值

    Raises:
        ValueError: 值类型错误
    """
    # 获取当前值以推断类型
    current_value = get_config_value(config, key)

    if current_value is None:
        # 新键,尝试自动推断类型
        return auto_parse_value(value_str)

    # 根据当前值的类型解析
    if isinstance(current_value, bool):
        return parse_bool(value_str)
    elif isinstance(current_value, int):
        return parse_int(value_str)
    elif isinstance(current_value, float):
        return parse_float(value_str)
    elif isinstance(current_value, list):
        return parse_list(value_str)
    elif isinstance(current_value, dict):
        return parse_dict(value_str)
    else:
        # 字符串
        return value_str

def parse_bool(value_str: str) -> bool:
    """解析布尔值"""
    lower = value_str.lower()
    if lower in ["true", "yes", "1"]:
        return True
    elif lower in ["false", "no", "0"]:
        return False
    else:
        raise ValueError(f"无效的布尔值: {value_str},使用 true 或 false")

def parse_int(value_str: str) -> int:
    """解析整数"""
    try:
        return int(value_str)
    except ValueError:
        raise ValueError(f"无效的整数: {value_str}")

def parse_float(value_str: str) -> float:
    """解析浮点数"""
    try:
        return float(value_str)
    except ValueError:
        raise ValueError(f"无效的浮点数: {value_str}")

def parse_list(value_str: str) -> list:
    """解析列表JSON 格式)"""
    try:
        value = json.loads(value_str)
        if not isinstance(value, list):
            raise ValueError("不是列表")
        return value
    except json.JSONDecodeError:
        raise ValueError(f"无效的列表格式: {value_str},使用 JSON 格式")

def parse_dict(value_str: str) -> dict:
    """解析字典JSON 格式)"""
    try:
        value = json.loads(value_str)
        if not isinstance(value, dict):
            raise ValueError("不是字典")
        return value
    except json.JSONDecodeError:
        raise ValueError(f"无效的字典格式: {value_str},使用 JSON 格式")

def auto_parse_value(value_str: str):
    """自动推断并解析值"""
    # 尝试 JSON
    try:
        return json.loads(value_str)
    except json.JSONDecodeError:
        pass

    # 尝试布尔值
    if value_str.lower() in ["true", "false", "yes", "no"]:
        return parse_bool(value_str)

    # 尝试数字
    try:
        if "." in value_str:
            return float(value_str)
        else:
            return int(value_str)
    except ValueError:
        pass

    # 默认为字符串
    return value_str

3.3.3 获取可用配置键

def get_available_keys(config: dict, prefix: str = "") -> list[str]:
    """获取所有可用的配置键

    Args:
        config: 配置字典
        prefix: 键前缀

    Returns:
        配置键列表
    """
    keys = []

    for key, value in config.items():
        full_key = f"{prefix}{key}" if prefix else key

        if isinstance(value, dict):
            # 递归获取嵌套键
            keys.extend(get_available_keys(value, f"{full_key}."))
        else:
            keys.append(full_key)

    return sorted(keys)

def is_valid_config_key(key: str) -> bool:
    """验证配置键是否有效

    Args:
        key: 配置键

    Returns:
        是否有效
    """
    # 定义允许的配置键
    valid_keys = [
        "task.source",
        "task.spec",
        "env.python.version",
        "env.python.venv",
        "env.tools.uv",
        "env.tools.git",
        "flow.phases",
        "flow.flowchart_dir",
        "decide.port",
        "decide.decisions_dir",
        "output.color",
        "output.language"
    ]

    return key in valid_keys

四、错误处理

4.1 错误分类

错误类型 退出码 处理方式
缺少参数 2 显示用法
配置文件不存在 4 提示运行 aide init
配置键不存在 4 显示可用的键
配置值类型错误 4 显示期望类型和建议
配置文件格式错误 4 显示错误位置

4.2 类型验证

def validate_config_value(key: str, value) -> list[str]:
    """验证配置值

    Args:
        key: 配置键
        value: 配置值

    Returns:
        错误列表(空列表表示无错误)
    """
    errors = []

    # 验证特定键的值
    if key == "env.python.version":
        if not isinstance(value, str):
            errors.append("env.python.version 必须是字符串")
        elif not is_valid_version_spec(value):
            errors.append(f"无效的版本格式: {value}")

    elif key == "env.python.venv":
        if not isinstance(value, str):
            errors.append("env.python.venv 必须是字符串")

    elif key in ["env.tools.uv", "env.tools.git", "output.color"]:
        if not isinstance(value, bool):
            errors.append(f"{key} 必须是布尔值")

    elif key == "flow.phases":
        if not isinstance(value, list):
            errors.append("flow.phases 必须是数组")
        elif not all(isinstance(p, str) for p in value):
            errors.append("flow.phases 的元素必须是字符串")

    elif key == "decide.port":
        if not isinstance(value, int):
            errors.append("decide.port 必须是整数")
        elif not (1024 <= value <= 65535):
            errors.append("decide.port 必须在 1024-65535 之间")

    return errors

五、测试用例

5.1 config get 测试

def test_config_get_success(tmp_path, monkeypatch, capsys):
    """测试获取配置成功"""
    monkeypatch.chdir(tmp_path)

    # 初始化配置
    cmd_init([])

    # 获取配置
    result = config_get("task.source")

    # 验证退出码
    assert result == 0

    # 验证输出
    captured = capsys.readouterr()
    assert 'task.source = "task-now.md"' in captured.out

def test_config_get_not_found(tmp_path, monkeypatch, capsys):
    """测试配置键不存在"""
    monkeypatch.chdir(tmp_path)

    # 初始化配置
    cmd_init([])

    # 获取不存在的配置
    result = config_get("invalid.key")

    # 验证退出码
    assert result == 4

    # 验证输出
    captured = capsys.readouterr()
    assert "配置键不存在" in captured.out

def test_config_get_no_config(tmp_path, monkeypatch, capsys):
    """测试配置文件不存在"""
    monkeypatch.chdir(tmp_path)

    # 获取配置(未初始化)
    result = config_get("task.source")

    # 验证退出码
    assert result == 4

    # 验证输出
    captured = capsys.readouterr()
    assert "配置文件不存在" in captured.out

5.2 config set 测试

def test_config_set_success(tmp_path, monkeypatch):
    """测试设置配置成功"""
    monkeypatch.chdir(tmp_path)

    # 初始化配置
    cmd_init([])

    # 设置配置
    result = config_set("task.source", "new-task.md")

    # 验证退出码
    assert result == 0

    # 验证配置已更新
    config = Config(tmp_path)
    config.load()
    assert config.get("task.source") == "new-task.md"

def test_config_set_bool(tmp_path, monkeypatch):
    """测试设置布尔值"""
    monkeypatch.chdir(tmp_path)

    # 初始化配置
    cmd_init([])

    # 设置布尔值
    result = config_set("env.tools.uv", "true")
    assert result == 0

    # 验证
    config = Config(tmp_path)
    config.load()
    assert config.get("env.tools.uv") == True

def test_config_set_invalid_type(tmp_path, monkeypatch, capsys):
    """测试设置错误类型"""
    monkeypatch.chdir(tmp_path)

    # 初始化配置
    cmd_init([])

    # 设置错误类型
    result = config_set("env.tools.git", "yes")

    # 验证退出码
    assert result == 4

    # 验证输出
    captured = capsys.readouterr()
    assert "类型错误" in captured.out

5.3 值解析测试

def test_parse_bool():
    """测试布尔值解析"""
    assert parse_bool("true") == True
    assert parse_bool("false") == False
    assert parse_bool("yes") == True
    assert parse_bool("no") == False

    with pytest.raises(ValueError):
        parse_bool("invalid")

def test_parse_list():
    """测试列表解析"""
    result = parse_list('["a", "b", "c"]')
    assert result == ["a", "b", "c"]

    with pytest.raises(ValueError):
        parse_list("not a list")

def test_auto_parse_value():
    """测试自动解析"""
    assert auto_parse_value("true") == True
    assert auto_parse_value("123") == 123
    assert auto_parse_value("3.14") == 3.14
    assert auto_parse_value('["a"]') == ["a"]
    assert auto_parse_value("text") == "text"

六、使用示例

6.1 查看配置

# 查看任务文档路径
aide config get task.source

# 查看 Python 版本要求
aide config get env.python.version

# 查看流程环节列表
aide config get flow.phases

6.2 修改配置

# 修改任务文档路径
aide config set task.source "my-task.md"

# 修改 Python 版本要求
aide config set env.python.version ">=3.11"

# 启用 uv
aide config set env.tools.uv true

# 修改端口
aide config set decide.port 8080

七、性能要求

7.1 执行时间

  • config get< 100ms
  • config set< 200ms

7.2 资源占用

  • 内存:< 20MB
  • 磁盘 I/O最小化

八、总结

8.1 核心要点

  1. 支持 get 和 set 两个子命令
  2. 点号分隔的键访问
  3. 自动类型推断和验证
  4. 静默原则set 成功时无输出)
  5. 清晰的错误信息

8.2 实现检查清单

  • 实现 cmd_config 函数
  • 实现 config_get 函数
  • 实现 config_set 函数
  • 实现值格式化函数
  • 实现值解析函数
  • 实现类型验证
  • 编写单元测试
  • 编写集成测试
  • 性能测试

版本v1.0 更新日期2025-12-13