📃 docs: 部分完成
This commit is contained in:
745
aide-program/docs/04-aide-config设计.md
Normal file
745
aide-program/docs/04-aide-config设计.md
Normal file
@@ -0,0 +1,745 @@
|
||||
# aide config 设计
|
||||
|
||||
## 一、命令概述
|
||||
|
||||
### 1.1 功能定位
|
||||
|
||||
`aide config` 命令用于读取和修改项目配置文件,提供命令行方式访问配置。
|
||||
|
||||
### 1.2 执行时机
|
||||
|
||||
- 用户需要查看配置值时
|
||||
- 用户需要修改配置值时
|
||||
- Commands 中需要获取配置时(如 prep/exec 的默认文档路径)
|
||||
|
||||
### 1.3 命令格式
|
||||
|
||||
```bash
|
||||
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 函数接口
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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 格式化配置值
|
||||
|
||||
```python
|
||||
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 解析配置值
|
||||
|
||||
```python
|
||||
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 获取可用配置键
|
||||
|
||||
```python
|
||||
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 类型验证
|
||||
|
||||
```python
|
||||
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 测试
|
||||
|
||||
```python
|
||||
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 测试
|
||||
|
||||
```python
|
||||
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 值解析测试
|
||||
|
||||
```python
|
||||
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 查看配置
|
||||
|
||||
```bash
|
||||
# 查看任务文档路径
|
||||
aide config get task.source
|
||||
|
||||
# 查看 Python 版本要求
|
||||
aide config get env.python.version
|
||||
|
||||
# 查看流程环节列表
|
||||
aide config get flow.phases
|
||||
```
|
||||
|
||||
### 6.2 修改配置
|
||||
|
||||
```bash
|
||||
# 修改任务文档路径
|
||||
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
|
||||
Reference in New Issue
Block a user