Merge d90fa21e80 into 2e149f44dd
This commit is contained in:
commit
919a9173d4
5 changed files with 1004 additions and 5 deletions
228
docs/zh/config-versioning.md
Normal file
228
docs/zh/config-versioning.md
Normal 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 配置现在直接迁移到 CurrentVersion(V2),而不是经过 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
125
docs/zh/cron.md
Normal 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` 中
|
||||
578
docs/zh/security_configuration.md
Normal file
578
docs/zh/security_configuration.md
Normal 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"
|
||||
```
|
||||
|
||||
系统在加载配置时会在运行时自动解密密钥。
|
||||
|
||||
### 好处
|
||||
|
||||
- 额外的安全层
|
||||
- 密钥静态加密
|
||||
- 密码短语可以与配置文件分开管理
|
||||
|
||||
### 重要说明
|
||||
|
||||
- 始终安全地备份您的密码短语
|
||||
- 如果丢失密码短语,您将失去对加密密钥的访问权限
|
||||
- 使用强且唯一的密码短语
|
||||
- 永不将密码短语提交到版本控制
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue