This commit is contained in:
WangMiao 2026-04-13 07:59:43 +00:00 committed by GitHub
commit 919a9173d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1004 additions and 5 deletions

View file

@ -0,0 +1,228 @@
# 配置文件版本控制指南
## 概述
PicoClaw 使用模式版本控制系统来管理 `config.json`,确保在配置格式演进时能够顺利升级。
## 版本历史
### 版本 1
- **引入时间**:初始版本,支持 version 字段
- **变更**:向 Config 结构体添加了 `version` 字段
- **迁移**:现有配置无需结构性变更
### 版本 2
- **引入时间**:模型启用/禁用支持和渠道配置统一
- **变更**
- 向 `ModelConfig` 添加了 `enabled` 字段 — 允许禁用单个模型条目而无需删除
- 在 V1→V2 迁移期间,`enabled` 会自动推断:带有 API 密钥或保留名称 `local-model` 的模型会被启用;其他模型默认禁用
- 迁移了旧版渠道字段Discord `mention_only``group_trigger.mention_only`OneBot `group_trigger_prefix``group_trigger.prefixes`
- V0 配置现在直接迁移到 CurrentVersionV2而不是经过 V1
- `makeBackup()` 现在使用仅日期后缀(如 `config.json.20260330.bak`),同时也会备份 `.security.yml`
## 工作原理
### 自动迁移
加载配置文件时:
1. 系统首先从 JSON 中读取 `version` 字段
2. 根据检测到的版本,加载相应的配置结构体(`configV0``configV1` 等)
3. 如果加载的版本低于最新版本,则增量应用迁移
4. 保存前,系统会自动创建 `config.json``.security.yml` 的带日期戳备份
5. 版本号会自动更新
6. 迁移后的配置会自动保存到磁盘
### 版本字段
`config.json` 中的 `version` 字段表示模式版本:
- `0` 或缺失:旧版配置(无 version 字段)
- `1`:上一版本(加载时会自动迁移到 V2
- `2`:当前版本
```json
{
"version": 2,
"agents": {...},
...
}
```
## 添加新的迁移
对配置模式进行破坏性变更时:
### 步骤 1定义新版本结构体
如果结构发生重大变更,创建新版本的结构体:
```go
// ConfigV2 代表版本 2 的配置结构
type ConfigV2 struct {
Version int `json:"version"`
Agents AgentsConfig `json:"agents"`
// ... 其他字段,新结构
}
```
### 步骤 2更新当前配置版本
```go
const CurrentVersion = 2 // 递增此值
```
### 步骤 3添加加载器函数
```go
// loadConfigV3 加载版本 3 的配置
func loadConfigV3(data []byte) (*Config, error) {
cfg := DefaultConfig()
// 解析为 ConfigV3 结构体
var v3 ConfigV3
if err := json.Unmarshal(data, &v3); err != nil {
return nil, err
}
// 转换为当前 Config
cfg.Version = v3.Version
cfg.Agents = v3.Agents
// ... 映射其他字段
return cfg, nil
}
```
### 步骤 4添加迁移逻辑
```go
func (c *configV2) Migrate() (*Config, error) {
// 在此处应用 V2→V3 的结构性变更
migrated := &c.Config
migrated.Version = 3
// 应用结构性变更
return migrated, nil
}
```
### 步骤 5更新 LoadConfig 开关
```go
func LoadConfig(path string) (*Config, error) {
// ... 读取文件 ...
switch versionInfo.Version {
case 0:
cfg, err = loadConfigV0(data)
case 1:
cfg, err = loadConfigV1(data)
case 2:
cfg, err = loadConfig(data)
case 3:
cfg, err = loadConfigV3(data)
default:
return nil, fmt.Errorf("unsupported config version: %d", versionInfo.Version)
}
// ... 迁移和验证 ...
}
```
### 步骤 6测试迁移
`config_migration_test.go` 中创建测试:
```go
func TestMigrateV2ToV3(t *testing.T) {
// 创建版本 2 的配置
v2Config := Config{
Version: 2,
// ... 设置测试数据
}
// 应用迁移
migrated, err := v2Config.Migrate()
if err != nil {
t.Fatalf("Migration failed: %v", err)
}
// 验证版本已更新
if migrated.Version != 3 {
t.Errorf("Expected version 3, got %d", migrated.Version)
}
// 验证数据正确保留/转换
// ...
}
```
## 迁移最佳实践
1. **版本特定结构体**:为每个有结构性变更的版本定义单独的结构体
2. **向后兼容**:确保旧配置仍能用其特定结构体加载
3. **无数据丢失**:迁移应保留所有用户设置
4. **幂等性**:多次运行相同迁移应该是安全的
5. **自动保存**:迁移后的配置会自动保存以更新用户的文件
6. **自动备份**:保存前,系统会创建 `config.json``.security.yml` 的带日期戳备份
7. **全面测试**:使用真实用户配置文件进行测试
8. **更新默认值**:使 `defaults.go` 与最新模式保持同步
## 迁移示例
### 场景:添加带默认值的新字段
旧配置(版本 2
```json
{
"version": 2,
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4"
}
]
}
```
迁移到版本 3
```go
func (c *configV2) Migrate() (*Config, error) {
migrated := &c.Config
migrated.Version = 3
// 如果未设置,添加带默认值的新字段
// ...
return migrated, nil
}
```
新配置(版本 3
```json
{
"version": 3,
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"new_option": true
}
]
}
```
## 故障排除
### 配置未升级
- 检查 `CurrentVersion` 是否已递增
- 验证迁移逻辑是否处理目标版本
- 确保 `Migrate()``LoadConfig()` 中被调用
### 迁移错误
- 检查错误消息以获取特定迁移失败信息
- 审查迁移逻辑的边缘情况
- 确保所有必需字段正确初始化
- 验证源版本的加载器函数
### 迁移后数据丢失
- 确保迁移期间所有字段都被复制
- 检查迁移不会不必要地用默认值覆盖值
- 审查加载器函数中的转换逻辑
- 检查自动备份文件(如 `config.json.20260330.bak`)以恢复原始数据

125
docs/zh/cron.md Normal file
View file

@ -0,0 +1,125 @@
# 定时任务与 Cron 作业
> 返回 [README](../README.md)
PicoClaw 将定时作业存储在当前工作区中,可以将它们作为提醒、完全自主的 Agent 回合或 shell 命令来运行。
## 调度类型
PicoClaw 目前在 cron 工具中使用三种调度形式:
- `at_seconds`:一次性任务,相对于当前时间。运行后,作业会从存储中删除。
- `every_seconds`:重复间隔,以秒为单位。
- `cron_expr`:重复的 cron 表达式,如 `0 9 * * *`
CLI 命令 `picoclaw cron add` 目前仅支持重复作业:
- `--every <seconds>`
- `--cron '<expr>'`
目前没有用于一次性 `at` 作业的 CLI 参数。
示例:
```bash
picoclaw cron add --name "Daily summary" --message "Summarize today's logs" --cron "0 18 * * *"
picoclaw cron add --name "Ping" --message "heartbeat" --every 300 --deliver
```
## 执行模式
作业存储时带有消息负载,可以以三种稳定的面向用户的模式执行:
### `deliver: false`
这是 cron 工具的默认值。
当作业触发时PicoClaw 会将保存的消息作为新的 Agent 回合发送回 Agent 循环。对于可能需要推理、工具或生成回复的定时工作使用此模式。
### `deliver: true`
当作业触发时PicoClaw 会将保存的消息直接发布到目标渠道和接收者,无需 Agent 处理。
CLI `picoclaw cron add --deliver` 参数使用此模式。
### `command`
当 cron-tool 作业包含 `command`PicoClaw 会通过 `exec` 工具运行该 shell 命令,并将命令输出发布回渠道。
对于命令作业,创建时会将 `deliver` 强制设为 `false`。保存的 `message` 仅作为描述性文本;定时动作是 shell 命令。
当前的 CLI `picoclaw cron add` 命令不暴露 `command` 参数。
## 配置与安全门控
### `tools.cron`
`tools.cron.enabled` 控制面向 Agent 的 `cron` 工具是否注册。默认值:`true`
如果禁用 `tools.cron`,用户将无法再通过 Agent 工具创建或管理作业。网关仍会启动 `CronService`但不会安装作业执行回调。因此定时作业不会实际运行一次性作业可能会被删除重复作业可能会被重新调度而不会执行其负载。CLI 仍使用相同的作业存储。
`tools.cron.exec_timeout_minutes` 设置定时命令执行的超时时间。默认值:`5`。设为 `0` 表示无超时。
### `tools.exec`
定时命令作业依赖于 `tools.exec.enabled`。默认值:`true`
如果 `tools.exec.enabled``false`
- 新的命令作业会被 cron 工具拒绝
- 现有命令作业在触发时会发布 `command execution is disabled` 错误
`tools.exec.allow_remote` 仍由 exec 工具强制执行,但 cron 命令调度在创建作业时已经需要内部渠道。实际上,提醒作业可以从远程渠道调度,而定时命令作业仅限于内部渠道。
### `allow_command`
`tools.cron.allow_command` 默认为 `true`
这不是硬禁用开关。如果将 `allow_command` 设为 `false`PicoClaw 仍会在调用者明确传递 `command_confirm: true` 时允许命令作业。
命令作业还需要内部渠道。非命令提醒没有此限制。
示例:
```json
{
"tools": {
"cron": {
"enabled": true,
"exec_timeout_minutes": 5,
"allow_command": true
},
"exec": {
"enabled": true
}
}
}
```
## 持久化与位置
Cron 作业存储在:
```text
<workspace>/cron/jobs.json
```
默认工作区是:
```text
~/.picoclaw/workspace
```
如果设置了 `PICOCLAW_HOME`,默认工作区变为:
```text
$PICOCLAW_HOME/workspace
```
网关和 `picoclaw cron` CLI 子命令使用相同的 `cron/jobs.json` 文件。
注意:
- 一次性 `at_seconds` 作业运行后会删除
- 重复作业保留在存储中直到被删除
- 禁用的作业保留在存储中,仍会显示在 `picoclaw cron list`

View file

@ -0,0 +1,578 @@
# 安全配置
## 概述
PicoClaw 支持通过将敏感数据API 密钥、令牌、密钥、密码)与主配置分离存储在 `.security.yml` 文件中来提高安全性:
1. **关注点分离**:配置设置和密钥在不同的文件中
2. **更易于共享**:主配置可以共享而不会暴露敏感数据
3. **更好的版本控制**`.security.yml` 应添加到 `.gitignore`
4. **灵活的部署**:不同环境可以使用不同的安全文件
## 文件结构
```
~/.picoclaw/
├── config.json # 主配置(可安全共享)
└── .security.yml # 安全数据(永不共享)
```
## 工作原理
安全配置通过**直接字段映射**工作,而不是通过 `ref:` 字符串引用。系统自动从 `.security.yml` 加载值并将其应用到 `config.json` 中的相应字段。
### 关键点:
- `.security.yml` 中的值会自动映射到配置中的相应字段
- 映射基于字段名称和结构,而不是引用字符串
- 如果 `.security.yml` 中存在某个值,它**会覆盖** `config.json` 中的值
- 您可以完全从 `config.json` 中省略敏感字段(推荐)
## 安全配置结构
### 完整示例:.security.yml
```yaml
# 模型 API 密钥
# 所有模型必须使用 `api_keys`(复数)数组格式
# 即使是单个密钥也必须作为包含一个元素的数组提供
model_list:
gpt-5.4:
api_keys:
- "sk-proj-your-actual-openai-key-1"
- "sk-proj-your-actual-openai-key-2" # 可选:用于故障转移的多个密钥
claude-sonnet-4.6:
api_keys:
- "sk-ant-your-actual-anthropic-key" # 数组格式中的单个密钥
# 渠道令牌
channels:
telegram:
token: "your-telegram-bot-token"
feishu:
app_secret: "your-feishu-app-secret"
encrypt_key: "your-feishu-encrypt-key"
verification_token: "your-feishu-verification-token"
discord:
token: "your-discord-bot-token"
weixin:
token: "your-weixin-token"
qq:
app_secret: "your-qq-app-secret"
dingtalk:
client_secret: "your-dingtalk-client-secret"
slack:
bot_token: "your-slack-bot-token"
app_token: "your-slack-app-token"
matrix:
access_token: "your-matrix-access-token"
line:
channel_secret: "your-line-channel-secret"
channel_access_token: "your-line-channel-access-token"
onebot:
access_token: "your-onebot-access-token"
wecom:
token: "your-wecom-token"
encoding_aes_key: "your-wecom-encoding-aes-key"
wecom_app:
corp_secret: "your-wecom-app-corp-secret"
token: "your-wecom-app-token"
encoding_aes_key: "your-wecom-app-encoding-aes-key"
wecom_aibot:
secret: "your-wecom-aibot-secret"
token: "your-wecom-aibot-token"
encoding_aes_key: "your-wecom-aibot-encoding-aes-key"
pico:
token: "your-pico-token"
irc:
password: "your-irc-password"
nickserv_password: "your-irc-nickserv-password"
sasl_password: "your-irc-sasl-password"
# Web 工具 API 密钥
web:
brave:
api_keys:
- "BSAyour-brave-api-key-1"
- "BSAyour-brave-api-key-2" # 可选:用于故障转移的多个密钥
tavily:
api_keys:
- "tvly-your-tavily-api-key" # 数组格式中的单个密钥
perplexity:
api_keys:
- "pplx-your-perplexity-api-key" # 数组格式中的单个密钥
glm_search:
api_key: "your-glm-search-api-key" # GLMSearch 使用单个密钥格式(不是数组)
baidu_search:
api_key: "your-baidu-search-api-key"
# 技能注册表令牌
skills:
github:
token: "your-github-token"
clawhub:
auth_token: "your-clawhub-auth-token"
```
## 使用方法
### 步骤 1创建 .security.yml
创建或复制安全文件:
```bash
cp security.example.yml ~/.picoclaw/.security.yml
```
### 步骤 2填写实际值
编辑 `~/.picoclaw/.security.yml`,用您实际的 API 密钥和令牌替换占位符值。
### 步骤 3设置正确的权限
```bash
chmod 600 ~/.picoclaw/.security.yml
```
### 步骤 4简化 config.json推荐
现在您可以从 `config.json` 中删除敏感字段,因为它们是从 `.security.yml` 加载的:
**之前:**
```json
{
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_base": "https://api.openai.com/v1",
"api_key": "sk-your-actual-api-key-here"
}
],
"channels": {
"telegram": {
"enabled": true,
"token": "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz"
}
}
}
```
**之后:**
```json
{
"model_list": [
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_base": "https://api.openai.com/v1"
// api_key 现在从 .security.yml 加载
}
],
"channels": {
"telegram": {
"enabled": true
// token 现在从 .security.yml 加载
}
}
}
```
### 步骤 5验证
重启 PicoClaw 并验证它正确加载:
```bash
picoclaw --version
```
## 字段映射规则
### 模型
**在 .security.yml 中:**
```yaml
model_list:
<model_name>:
api_keys:
- "key-1"
- "key-2"
```
**映射:**
- 字段 `api_keys`(数组)映射到模型的 API 密钥
- `<model_name>` 必须与 `config.json` 中的 `model_name` 字段匹配
- 支持索引名称(如 "gpt-5.4:0")— 系统也会尝试基础名称("gpt-5.4"
### 渠道
每个渠道直接映射其字段:
**在 .security.yml 中:**
```yaml
channels:
telegram:
token: "value"
feishu:
app_secret: "value"
encrypt_key: "value"
verification_token: "value"
discord:
token: "value"
```
**映射:**
- `channels.telegram.token``config.channels.telegram.token`
- `channels.feishu.app_secret``config.channels.feishu.app_secret`
- 等等。
### Web 工具
**Brave、Tavily、Perplexity**
```yaml
web:
brave:
api_keys:
- "key-1"
- "key-2"
```
- 使用 `api_keys`(复数)数组格式
**GLMSearch**
```yaml
web:
glm_search:
api_key: "single-key-here"
```
- 使用 `api_key`(单数)单个字符串格式
**BaiduSearch**
```yaml
web:
baidu_search:
api_key: "your-key"
```
- 使用 `api_key`(单数)单个字符串格式
### 技能
**在 .security.yml 中:**
```yaml
skills:
github:
token: "value"
clawhub:
auth_token: "value"
```
## API 密钥格式
### 模型 - 单个密钥
使用包含一个元素的数组格式:
```yaml
model_list:
gpt-5.4:
api_keys:
- "sk-your-key"
```
### 模型 - 多个密钥(负载均衡与故障转移)
使用包含多个元素的数组格式:
```yaml
model_list:
gpt-5.4:
api_keys:
- "sk-your-key-1"
- "sk-your-key-2"
- "sk-your-key-3"
```
**好处:**
- **负载均衡**:请求分布在多个密钥之间
- **故障转移**:如果一个密钥失败,自动切换到另一个密钥
- **速率限制管理**:在多个密钥之间分配使用
- **高可用性**:减少 API 提供商问题期间的停机时间
### Web 工具Brave/Tavily/Perplexity- 单个密钥
```yaml
web:
brave:
api_keys:
- "BSA-your-key"
```
### Web 工具Brave/Tavily/Perplexity- 多个密钥
```yaml
web:
brave:
api_keys:
- "BSA-key-1"
- "BSA-key-2"
```
### Web 工具GLMSearch/BaiduSearch- 仅限单个密钥
```yaml
web:
glm_search:
api_key: "your-glm-key" # 单个字符串(不是数组)
baidu_search:
api_key: "your-baidu-key" # 单个字符串(不是数组)
```
## 模型名称匹配
系统支持 `.security.yml` 中智能模型名称匹配:
### 示例 1精确匹配
**config.json**
```json
{
"model_name": "gpt-5.4:0"
}
```
**.security.yml带索引的精确匹配**
```yaml
model_list:
gpt-5.4:0:
api_keys: ["key-1"]
```
### 示例 2基础名称匹配
**config.json**
```json
{
"model_name": "gpt-5.4:0"
}
```
**.security.yml不带索引的基础名称**
```yaml
model_list:
gpt-5.4:
api_keys: ["key-1", "key-2"]
```
两种方法都有效。基础名称匹配允许您在配置使用索引模型名称进行负载均衡时在 `.security.yml` 中使用更简单的密钥。
## 向后兼容
系统保持完全向后兼容:
1. **直接值**:您仍可以在 `config.json` 中使用直接值(不推荐用于生产环境)
2. **混合使用**:您可以在 `.security.yml``config.json` 中都有某些字段
3. **可选安全文件**:如果 `.security.yml` 不存在,系统将仅使用 `config.json` 中的值
4. **覆盖行为**:如果两个文件都存在某字段,`.security.yml` 的值优先
## 环境变量
您可以使用环境变量覆盖任何安全值:
**对于模型:**
```bash
export PICOCLAW_CHANNELS_TELEGRAM_TOKEN="token-from-env"
```
**对于渠道:**
```bash
export PICOCLAW_CHANNELS_TELEGRAM_TOKEN="token-from-env"
export PICOCLAW_CHANNELS_FEISHU_APP_SECRET="secret-from-env"
```
**对于 Web 工具:**
```bash
export PICOCLAW_TOOLS_WEB_BRAVE_API_KEY="key-from-env"
export PICOCLAW_TOOLS_WEB_BAIDU_API_KEY="baidu-key-from-env"
```
环境变量具有最高优先级,会覆盖 `config.json``.security.yml` 的值。
格式为:`PICOCLAW_<SECTION>_<KEY>_<FIELD>`,用下划线分隔路径段并转换为大写。
## 安全最佳实践
1. **永不提交 `.security.yml`** 到版本控制
2. **添加到 .gitignore**:确保 `.security.yml` 在您的 `.gitignore` 文件中
3. **设置文件权限**`chmod 600 ~/.picoclaw/.security.yml`
4. **不同环境使用不同密钥**(开发、暂存、生产)
5. **定期轮换密钥**并更新 `.security.yml`
6. **安全备份**:加密包含 `.security.yml` 的备份。请注意,配置迁移会自动创建带日期戳的备份(如 `config.json.20260330.bak``.security.yml.20260330.bak`
7. **审查访问权限**:确保只有授权用户能读取该文件
## API
### loadSecurityConfig
```go
func loadSecurityConfig(securityPath string) (*SecurityConfig, error)
```
`.security.yml` 加载安全配置。如果文件不存在,返回空的 `SecurityConfig`
### saveSecurityConfig
```go
func saveSecurityConfig(securityPath string, sec *SecurityConfig) error
```
`0o600` 权限将安全配置保存到 `.security.yml`
### applySecurityConfig
```go
func applySecurityConfig(cfg *Config, sec *SecurityConfig) error
```
通过将值从 `.security.yml` 复制到配置的相应字段来应用安全配置。
### securityPath
```go
func securityPath(configPath string) string
```
返回配置文件中 `.security.yml` 的相对路径。
## 测试
运行安全配置测试:
```bash
go test ./pkg/config -run TestSecurityConfig
```
## 故障排除
### 错误:"failed to load security config"
- 验证 `.security.yml` 存在于 `config.json` 的同一目录中
- 检查 YAML 语法是否有效(使用 YAML 验证器)
- 确保文件权限允许读取
### 错误:"model security entry not found"
- 确保 `config.json` 中的模型名称与 `.security.yml` 中的完全匹配
- 检查 `.security.yml` 中存在 `model_list` 部分
- 对于带索引名称的模型(如 "gpt-5.4:0"),确保使用确切的名称或不带索引的基础名称
- 验证 YAML 结构正确(正确的缩进)
### 多个 API 密钥不工作
- 确保对模型和 Web 工具使用 `api_keys`复数GLMSearch/BaiduSearch 除外)
- 检查数组格式在 YAML 中正确(破折号的正确缩进)
- 记住模型、Brave、Tavily、Perplexity 必须使用 `api_keys`(数组格式)
- GLMSearch 和 BaiduSearch 必须使用 `api_key`(单个字符串格式)
### 负载均衡/故障转移问题
- 验证 `api_keys` 数组中的所有 API 密钥都有效
- 检查所有密钥具有相同的速率限制和权限
- 监控日志以查看正在使用哪些密钥以及哪些失败
- 确保 `api_keys` 数组在 YAML 中格式正确
### 密钥未被应用
- 检查 `.security.yml``config.json` 在同一目录中
- 验证文件权限允许读取(`chmod 600 ~/.picoclaw/.security.yml`
- 确保 YAML 结构与预期格式匹配
- 检查字段名称中的拼写错误(区分大小写)
- 验证模型/渠道名称完全匹配(区分大小写)
## 迁移指南
### 步骤 1备份您的配置
系统在保存迁移后的配置前会自动创建带日期戳的备份(如 `config.json.20260330.bak``.security.yml.20260330.bak`)。如果您更喜欢手动备份:
```bash
cp ~/.picoclaw/config.json ~/.picoclaw/config.json.backup
```
### 步骤 2创建 .security.yml
```bash
cp security.example.yml ~/.picoclaw/.security.yml
```
### 步骤 3填写您的 API 密钥
编辑 `~/.picoclaw/.security.yml`,用您实际的密钥替换占位符值。
### 步骤 4从 config.json 中删除敏感字段
`config.json` 中删除或注释敏感字段:
- `model_list` 条目中的 `api_key` 字段
- `channels` 中的 `token` 字段
- `tools.web` 中的 `api_key` 字段
- `tools.skills` 中的 `token`/`auth_token` 字段
### 步骤 5设置正确的权限
```bash
chmod 600 ~/.picoclaw/.security.yml
```
### 步骤 6测试
```bash
picoclaw --version
```
### 步骤 7验证功能
测试您的模型和渠道以确保一切正常工作。
### 步骤 8清理可选
如果一切正常,您可以删除备份:
```bash
rm ~/.picoclaw/config.json.backup
# 也可以删除自动生成的带日期戳的备份:
rm ~/.picoclaw/config.json.20*.bak ~/.picoclaw/.security.yml.20*.bak
```
## 高级:加密 API 密钥
PicoClaw 支持加密安全文件中的 API 密钥以提供额外保护。
### 设置
1. 通过环境变量设置密码短语:
```bash
export PICOCLAW_CREDENTIAL_PASSPHRASE="your-secure-passphrase"
```
2. 保存配置时API 密钥将自动加密:
```go
SaveConfig(path, config)
```
### 加密格式
加密密钥存储为:
```yaml
model_list:
gpt-5.4:
api_keys:
- "enc://encrypted-base64-string"
```
系统在加载配置时会在运行时自动解密密钥。
### 好处
- 额外的安全层
- 密钥静态加密
- 密码短语可以与配置文件分开管理
### 重要说明
- 始终安全地备份您的密码短语
- 如果丢失密码短语,您将失去对加密密钥的访问权限
- 使用强且唯一的密码短语
- 永不将密码短语提交到版本控制

View file

@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
@ -42,8 +43,25 @@ var (
mu sync.RWMutex
writers []io.Writer
consoleWriter zerolog.ConsoleWriter
// controlCharPattern matches ANSI escape sequences and Unicode format
// characters that can alter terminal rendering or mislead operators.
controlCharPattern = regexp.MustCompile(`(\x1b\[[0-9;]*[a-zA-Z]|\x1b[()][AB012]|\x1b[>?[0-9]+[a-z]|\x1b\][^\x07]*\x07?|\x1b[^a-zA-Z]*[a-zA-Z]|\u202[aeo]|\u200f|\u200e|\u2066|\u2067|\u2068|\u2069)`)
)
// escapeControlChars replaces terminal control characters and Unicode format
// characters with their safe escaped representation. This prevents log output
// from altering terminal state or misleading operators.
func escapeControlChars(s string) string {
return controlCharPattern.ReplaceAllStringFunc(s, func(match string) string {
var buf strings.Builder
for _, r := range match {
fmt.Fprintf(&buf, "\\x%02x", r)
}
return buf.String()
})
}
func init() {
once.Do(func() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
@ -88,13 +106,17 @@ func formatFieldValue(i any) string {
case []byte:
s = string(val)
default:
return fmt.Sprintf("%v", i)
return escapeControlChars(fmt.Sprintf("%v", i))
}
if unquoted, err := strconv.Unquote(s); err == nil {
s = unquoted
}
// Escape terminal control and format characters to prevent
// log output from altering terminal state or misleading operators.
s = escapeControlChars(s)
if strings.Contains(s, "\n") {
return fmt.Sprintf("\n%s", s)
}

View file

@ -28,6 +28,23 @@ var (
sessionManagerMu sync.RWMutex
)
// controlCharPattern matches ANSI escape sequences and Unicode format characters
// that can alter terminal rendering or be used for malicious purposes.
var controlCharPattern = regexp.MustCompile(`(\x1b\[[0-9;]*[a-zA-Z]|\x1b[()][AB012]|\x1b[>?[0-9]+[a-z-z]|\x1b\][^\x07]*\x07?|\x1b[^a-zA-Z]*[a-zA-Z]|\u202[aeo]|\u200f|\u200e|\u2066|\u2067|\u2068|\u2069)`)
// escapeControlChars replaces terminal control characters and Unicode format
// characters with their safe escaped representation. This prevents commands
// from altering terminal state or misleading operators.
func escapeControlChars(s string) string {
return controlCharPattern.ReplaceAllStringFunc(s, func(match string) string {
var buf strings.Builder
for _, r := range match {
fmt.Fprintf(&buf, "\\x%02x", r)
}
return buf.String()
})
}
func getSessionManager() *SessionManager {
sessionManagerMu.RLock()
defer sessionManagerMu.RUnlock()
@ -443,6 +460,10 @@ func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult
output = "(no output)"
}
// Escape terminal control and format characters to prevent
// command output from altering terminal state or misleading operators.
output = escapeControlChars(output)
maxLen := 10000
if len(output) > maxLen {
output = output[:maxLen] + fmt.Sprintf("\n... (truncated, %d more chars)", len(output)-maxLen)
@ -706,6 +727,9 @@ func (t *ExecTool) executeRead(args map[string]any) *ToolResult {
}
output := session.Read()
// Escape terminal control and format characters to prevent
// malicious output from altering terminal state.
output = escapeControlChars(output)
resp := ExecResponse{
SessionID: sessionID,
@ -1054,15 +1078,37 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
}
if t.restrictToWorkspace {
if strings.Contains(cmd, "..\\") || strings.Contains(cmd, "../") {
return "Command blocked by safety guard (path traversal detected)"
}
cwdPath, err := filepath.Abs(cwd)
if err != nil {
return ""
}
// Check for path traversal patterns that need resolution.
// Instead of blanket-blocking ../, we resolve the path and verify
// it stays within the working directory.
if strings.Contains(cmd, "..\\") || strings.Contains(cmd, "../") {
// Extract path segments containing ..
// Match path-like segments that include ..
pathTraversalPattern := regexp.MustCompile(`(?:[.\w]+/)++\.\.(?:/[.\w]+)*|[.\w]+/+\.\.(?:/[.\w]+)*`)
traversalIndices := pathTraversalPattern.FindAllStringIndex(cmd, -1)
for _, loc := range traversalIndices {
traversalPath := cmd[loc[0]:loc[1]]
// Resolve the traversal path relative to cwd
resolved, err := filepath.Abs(filepath.Join(cwdPath, traversalPath))
if err != nil {
return "Command blocked by safety guard (path traversal detected)"
}
// Check if resolved path is still within cwd
rel, err := filepath.Rel(cwdPath, resolved)
if err != nil || strings.HasPrefix(rel, "..") {
return "Command blocked by safety guard (path traversal detected)"
}
}
}
// Web URL schemes whose path components (starting with //) should be exempt
// from workspace sandbox checks. file: is intentionally excluded so that
// file:// URIs are still validated against the workspace boundary.