Merge branch 'upstream/main' into docs/add-vietnamese-readme
Resolved conflicts in README.md, README.ja.md, and README.zh.md by keeping both Portuguese (upstream) and Vietnamese (local) language links.
This commit is contained in:
commit
1e88df3ea8
22 changed files with 1374 additions and 80 deletions
30
.github/pull_request_template.md
vendored
30
.github/pull_request_template.md
vendored
|
|
@ -1,4 +1,7 @@
|
||||||
## 📝 Description
|
## 📝 Description
|
||||||
|
|
||||||
|
<!-- Please briefly describe the changes and purpose of this PR -->
|
||||||
|
|
||||||
## 🗣️ Type of Change
|
## 🗣️ Type of Change
|
||||||
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
|
- [ ] 🐞 Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
||||||
|
|
@ -11,25 +14,28 @@
|
||||||
- [ ] 👨💻 Mostly Human-written (Human lead, AI assisted or none)
|
- [ ] 👨💻 Mostly Human-written (Human lead, AI assisted or none)
|
||||||
|
|
||||||
|
|
||||||
## 🔗 Linked Issue
|
## 🔗 Related Issue
|
||||||
|
|
||||||
|
<!-- Please link the related issue(s) (e.g., Fixes #123, Closes #456) -->
|
||||||
|
|
||||||
## 📚 Technical Context (Skip for Docs)
|
## 📚 Technical Context (Skip for Docs)
|
||||||
* **Reference:** [URL]
|
- **Reference URL:**
|
||||||
* **Reasoning:** ...
|
- **Reasoning:**
|
||||||
|
|
||||||
|
## 🧪 Test Environment
|
||||||
|
- **Hardware:** <!-- e.g. Raspberry Pi 5, Orange Pi, PC-->
|
||||||
|
- **OS:** <!-- e.g. Debian 12, Ubuntu 22.04 -->
|
||||||
|
- **Model/Provider:** <!-- e.g. OpenAI GPT-4o, Kimi k2, DeepSeek-V3 -->
|
||||||
|
- **Channels:** <!-- e.g. Discord, Telegram, Feishu, ... -->
|
||||||
|
|
||||||
|
|
||||||
## 🧪 Test Environment & Hardware
|
## 📸 Evidence (Optional)
|
||||||
- **Hardware:** [e.g. Raspberry Pi 5, Orange Pi, PC]
|
|
||||||
- **OS:** [e.g. Debian 12, Ubuntu 22.04]
|
|
||||||
- **Model/Provider:** [e.g. OpenAI GPT-4o, Kimi k2, DeepSeek-V3]
|
|
||||||
- **Channels:** [e.g. Discord, Telegram, Feishu, ...]
|
|
||||||
|
|
||||||
|
|
||||||
## 📸 Proof of Work (Optional for Docs)
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click to view Logs/Screenshots</summary>
|
<summary>Click to view Logs/Screenshots</summary>
|
||||||
|
|
||||||
</details>
|
<!-- Please paste relevant screenshots or logs here -->
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## ☑️ Checklist
|
## ☑️ Checklist
|
||||||
- [ ] My code/docs follow the style of this project.
|
- [ ] My code/docs follow the style of this project.
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -55,6 +55,7 @@ jobs:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
- name: Setup Go from go.mod
|
- name: Setup Go from go.mod
|
||||||
|
id: setup-go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
|
|
@ -89,6 +90,7 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||||
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
|
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
|
||||||
|
GOVERSION: ${{ steps.setup-go.outputs.go-version }}
|
||||||
|
|
||||||
- name: Apply release flags
|
- name: Apply release flags
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,14 @@ builds:
|
||||||
- id: picoclaw
|
- id: picoclaw
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
|
tags:
|
||||||
|
- stdjson
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.version={{ .Version }}
|
||||||
|
- -X main.gitCommit={{ .ShortCommit }}
|
||||||
|
- -X main.buildTime={{ .Date }}
|
||||||
|
- -X main.goVersion={{ .Env.GOVERSION }}
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,14 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
# Copy binary
|
# Copy binary
|
||||||
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
|
||||||
|
|
||||||
# Create picoclaw home directory
|
# Create non-root user and group
|
||||||
|
RUN addgroup -g 1000 picoclaw && \
|
||||||
|
adduser -D -u 1000 -G picoclaw picoclaw
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER picoclaw
|
||||||
|
|
||||||
|
# Run onboard to create initial directories and config
|
||||||
RUN /usr/local/bin/picoclaw onboard
|
RUN /usr/local/bin/picoclaw onboard
|
||||||
|
|
||||||
ENTRYPOINT ["picoclaw"]
|
ENTRYPOINT ["picoclaw"]
|
||||||
|
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -11,11 +11,11 @@ VERSION?=$(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||||
GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev")
|
GIT_COMMIT=$(shell git rev-parse --short=8 HEAD 2>/dev/null || echo "dev")
|
||||||
BUILD_TIME=$(shell date +%FT%T%z)
|
BUILD_TIME=$(shell date +%FT%T%z)
|
||||||
GO_VERSION=$(shell $(GO) version | awk '{print $$3}')
|
GO_VERSION=$(shell $(GO) version | awk '{print $$3}')
|
||||||
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION)"
|
LDFLAGS=-ldflags "-X main.version=$(VERSION) -X main.gitCommit=$(GIT_COMMIT) -X main.buildTime=$(BUILD_TIME) -X main.goVersion=$(GO_VERSION) -s -w"
|
||||||
|
|
||||||
# Go variables
|
# Go variables
|
||||||
GO?=go
|
GO?=go
|
||||||
GOFLAGS?=-v
|
GOFLAGS?=-v -tags stdjson
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
INSTALL_PREFIX?=$(HOME)/.local
|
INSTALL_PREFIX?=$(HOME)/.local
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
|
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
|
||||||
|
|
||||||
<h3>$10 ハードウェア · 10MB RAM · 1秒起動 · 皮皮虾,我们走!</h3>
|
<h3>$10 ハードウェア · 10MB RAM · 1秒起動 · 行くぜ、シャコ!</h3>
|
||||||
<h3></h3>
|
<h3></h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**日本語** | [中文](README.zh.md) | [Tiếng Việt](README.vi.md) | [English](README.md)
|
[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [English](README.md)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## 📢 ニュース
|
## 📢 ニュース
|
||||||
2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 皮皮虾,我们走!
|
2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 行くぜ、シャコ!
|
||||||
|
|
||||||
## ✨ 特徴
|
## ✨ 特徴
|
||||||
|
|
||||||
|
|
@ -735,7 +735,7 @@ Discord: https://discord.gg/V4sAZ9XWpN
|
||||||
|
|
||||||
## 🐛 トラブルシューティング
|
## 🐛 トラブルシューティング
|
||||||
|
|
||||||
### Web 検索で「API 配置问题」と表示される
|
### Web 検索で「API 設定の問題」と表示される
|
||||||
|
|
||||||
検索 API キーをまだ設定していない場合、これは正常です。PicoClaw は手動検索用の便利なリンクを提供します。
|
検索 API キーをまだ設定していない場合、これは正常です。PicoClaw は手動検索用の便利なリンクを提供します。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[中文](README.zh.md) | [日本語](README.ja.md) | [Tiếng Việt](README.vi.md) | **English**
|
[中文](README.zh.md) | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | **English**
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
881
README.pt-br.md
Normal file
881
README.pt-br.md
Normal file
|
|
@ -0,0 +1,881 @@
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
<h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1>
|
||||||
|
|
||||||
|
<h3>Hardware de $10 · 10MB de RAM · Boot em 1s · 皮皮虾,我们走!</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
|
||||||
|
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20RISC--V-blue" alt="Hardware">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
||||||
|
<br>
|
||||||
|
<a href="https://picoclaw.io"><img src="https://img.shields.io/badge/Website-picoclaw.io-blue?style=flat&logo=google-chrome&logoColor=white" alt="Website"></a>
|
||||||
|
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[中文](README.zh.md) | [日本語](README.ja.md) | [English](README.md) | **Português**
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
🦐 **PicoClaw** é um assistente pessoal de IA ultra-leve inspirado no [nanobot](https://github.com/HKUDS/nanobot), reescrito do zero em **Go** por meio de um processo de "auto-inicialização" (self-bootstrapping) — onde o próprio agente de IA conduziu toda a migração de arquitetura e otimização de código.
|
||||||
|
|
||||||
|
⚡️ **Extremamente leve:** Roda em hardware de apenas **$10** com **<10MB** de RAM. Isso é 99% menos memória que o OpenClaw e 98% mais barato que um Mac mini!
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/picoclaw_mem.gif" width="360" height="240">
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="top">
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/licheervnano.png" width="400" height="240">
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
> [!CAUTION]
|
||||||
|
> **🚨 DECLARACAO DE SEGURANCA & CANAIS OFICIAIS**
|
||||||
|
>
|
||||||
|
> * **SEM CRIPTOMOEDAS:** O PicoClaw **NAO** possui nenhum token/moeda oficial. Todas as alegacoes no `pump.fun` ou outras plataformas de negociacao sao **GOLPES**.
|
||||||
|
> * **DOMINIO OFICIAL:** O **UNICO** site oficial e **[picoclaw.io](https://picoclaw.io)**, e o site da empresa e **[sipeed.com](https://sipeed.com)**.
|
||||||
|
> * **Aviso:** Muitos dominios `.ai/.org/.com/.net/...` foram registrados por terceiros, nao sao nossos.
|
||||||
|
> * **Aviso:** O PicoClaw esta em fase inicial de desenvolvimento e pode ter problemas de seguranca de rede nao resolvidos. Nao implante em ambientes de producao antes da versao v1.0.
|
||||||
|
> * **Nota:** O PicoClaw recentemente fez merge de muitos PRs, o que pode resultar em maior consumo de memoria (10-20MB) nas versoes mais recentes. Planejamos priorizar a otimizacao de recursos assim que o conjunto de funcionalidades estiver estavel.
|
||||||
|
|
||||||
|
|
||||||
|
## 📢 Novidades
|
||||||
|
|
||||||
|
2026-02-16 🎉 PicoClaw atingiu 12K stars em uma semana! Obrigado a todos pelo apoio! O PicoClaw esta crescendo mais rapido do que jamais imaginamos. Dado o alto volume de PRs, precisamos urgentemente de maintainers da comunidade. Nossos papeis de voluntarios e roadmap foram publicados oficialmente [aqui](docs/picoclaw_community_roadmap_260216.md) — estamos ansiosos para ter voce a bordo!
|
||||||
|
|
||||||
|
2026-02-13 🎉 PicoClaw atingiu 5000 stars em 4 dias! Obrigado a comunidade! Estamos finalizando o **Roadmap do Projeto** e configurando o **Grupo de Desenvolvedores** para acelerar o desenvolvimento do PicoClaw.
|
||||||
|
🚀 **Chamada para Acao:** Envie suas solicitacoes de funcionalidades nas GitHub Discussions. Revisaremos e priorizaremos na proxima reuniao semanal.
|
||||||
|
|
||||||
|
2026-02-09 🎉 PicoClaw lancado oficialmente! Construido em 1 dia para trazer Agentes de IA para hardware de $10 com <10MB de RAM. 🦐 PicoClaw, Partiu!
|
||||||
|
|
||||||
|
## ✨ Funcionalidades
|
||||||
|
|
||||||
|
🪶 **Ultra-Leve**: Consumo de memoria <10MB — 99% menor que o Clawdbot para funcionalidades essenciais.
|
||||||
|
|
||||||
|
💰 **Custo Minimo**: Eficiente o suficiente para rodar em hardware de $10 — 98% mais barato que um Mac mini.
|
||||||
|
|
||||||
|
⚡️ **Inicializacao Relampago**: Tempo de inicializacao 400X mais rapido, boot em 1 segundo mesmo em CPU single-core de 0.6GHz.
|
||||||
|
|
||||||
|
🌍 **Portabilidade Real**: Um unico binario auto-contido para RISC-V, ARM e x86. Um clique e ja era!
|
||||||
|
|
||||||
|
🤖 **Auto-Construido por IA**: Implementacao nativa em Go de forma autonoma — 95% do nucleo gerado pelo Agente com refinamento humano no loop.
|
||||||
|
|
||||||
|
| | OpenClaw | NanoBot | **PicoClaw** |
|
||||||
|
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
|
||||||
|
| **Linguagem** | TypeScript | Python | **Go** |
|
||||||
|
| **RAM** | >1GB | >100MB | **< 10MB** |
|
||||||
|
| **Inicializacao**</br>(CPU 0.8GHz) | >500s | >30s | **<1s** |
|
||||||
|
| **Custo** | Mac Mini $599 | Maioria dos SBC Linux </br>~$50 | **Qualquer placa Linux**</br>**A partir de $10** |
|
||||||
|
|
||||||
|
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
## 🦾 Demonstracao
|
||||||
|
|
||||||
|
### 🛠️ Fluxos de Trabalho Padrao do Assistente
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<th><p align="center">🧩 Engenharia Full-Stack</p></th>
|
||||||
|
<th><p align="center">🗂️ Gerenciamento de Logs & Planejamento</p></th>
|
||||||
|
<th><p align="center">🔎 Busca Web & Aprendizado</p></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
|
||||||
|
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
|
||||||
|
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">Desenvolver • Implantar • Escalar</td>
|
||||||
|
<td align="center">Agendar • Automatizar • Memorizar</td>
|
||||||
|
<td align="center">Descobrir • Analisar • Tendencias</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
### 📱 Rode em celulares Android antigos
|
||||||
|
|
||||||
|
De uma segunda vida ao seu celular de dez anos atras! Transforme-o em um assistente de IA inteligente com o PicoClaw. Inicio rapido:
|
||||||
|
|
||||||
|
1. **Instale o Termux** (Disponivel no F-Droid ou Google Play).
|
||||||
|
2. **Execute os comandos**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Nota: Substitua v0.1.1 pela versao mais recente da pagina de Releases
|
||||||
|
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
|
||||||
|
chmod +x picoclaw-linux-arm64
|
||||||
|
pkg install proot
|
||||||
|
termux-chroot ./picoclaw-linux-arm64 onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois siga as instrucoes na secao "Inicio Rapido" para completar a configuracao!
|
||||||
|
|
||||||
|
<img src="assets/termux.jpg" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
### 🐜 Implantacao Inovadora com Baixo Consumo
|
||||||
|
|
||||||
|
O PicoClaw pode ser implantado em praticamente qualquer dispositivo Linux!
|
||||||
|
|
||||||
|
- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versao E (Ethernet) ou W (WiFi6), para Assistente Domestico Minimalista
|
||||||
|
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), ou $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) para Manutencao Automatizada de Servidores
|
||||||
|
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) ou $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) para Monitoramento Inteligente
|
||||||
|
|
||||||
|
https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4
|
||||||
|
|
||||||
|
🌟 Mais cenarios de implantacao aguardam voce!
|
||||||
|
|
||||||
|
## 📦 Instalacao
|
||||||
|
|
||||||
|
### Instalar com binario pre-compilado
|
||||||
|
|
||||||
|
Baixe o binario para sua plataforma na pagina de [releases](https://github.com/sipeed/picoclaw/releases).
|
||||||
|
|
||||||
|
### Instalar a partir do codigo-fonte (funcionalidades mais recentes, recomendado para desenvolvimento)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/sipeed/picoclaw.git
|
||||||
|
|
||||||
|
cd picoclaw
|
||||||
|
make deps
|
||||||
|
|
||||||
|
# Build, sem necessidade de instalar
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Build para multiplas plataformas
|
||||||
|
make build-all
|
||||||
|
|
||||||
|
# Build e Instalar
|
||||||
|
make install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker Compose
|
||||||
|
|
||||||
|
Voce tambem pode rodar o PicoClaw usando Docker Compose sem instalar nada localmente.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone este repositorio
|
||||||
|
git clone https://github.com/sipeed/picoclaw.git
|
||||||
|
cd picoclaw
|
||||||
|
|
||||||
|
# 2. Configure suas API keys
|
||||||
|
cp config/config.example.json config/config.json
|
||||||
|
vim config/config.json # Configure DISCORD_BOT_TOKEN, API keys, etc.
|
||||||
|
|
||||||
|
# 3. Build & Iniciar
|
||||||
|
docker compose --profile gateway up -d
|
||||||
|
|
||||||
|
# 4. Ver logs
|
||||||
|
docker compose logs -f picoclaw-gateway
|
||||||
|
|
||||||
|
# 5. Parar
|
||||||
|
docker compose --profile gateway down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modo Agente (Execucao unica)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fazer uma pergunta
|
||||||
|
docker compose run --rm picoclaw-agent -m "Quanto e 2+2?"
|
||||||
|
|
||||||
|
# Modo interativo
|
||||||
|
docker compose run --rm picoclaw-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rebuild
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile gateway build --no-cache
|
||||||
|
docker compose --profile gateway up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚀 Inicio Rapido
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Configure sua API key em `~/.picoclaw/config.json`.
|
||||||
|
> Obtenha API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
||||||
|
> Busca web e **opcional** — obtenha a [Brave Search API](https://brave.com/search/api) gratuita (2000 consultas gratis/mes) ou use o fallback automatico integrado.
|
||||||
|
|
||||||
|
**1. Inicializar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Configurar** (`~/.picoclaw/config.json`)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tool_iterations": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openrouter": {
|
||||||
|
"api_key": "xxx",
|
||||||
|
"api_base": "https://openrouter.ai/api/v1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"web": {
|
||||||
|
"brave": {
|
||||||
|
"enabled": false,
|
||||||
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
|
"max_results": 5
|
||||||
|
},
|
||||||
|
"duckduckgo": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_results": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Obter API Keys**
|
||||||
|
|
||||||
|
* **Provedor de LLM**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
|
||||||
|
* **Busca Web** (opcional): [Brave Search](https://brave.com/search/api) - Plano gratuito disponivel (2000 consultas/mes)
|
||||||
|
|
||||||
|
> **Nota**: Veja `config.example.json` para um modelo de configuracao completo.
|
||||||
|
|
||||||
|
**4. Conversar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw agent -m "Quanto e 2+2?"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pronto! Voce tem um assistente de IA funcionando em 2 minutos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💬 Integracao com Apps de Chat
|
||||||
|
|
||||||
|
Converse com seu PicoClaw via Telegram, Discord, DingTalk ou LINE.
|
||||||
|
|
||||||
|
| Canal | Nivel de Configuracao |
|
||||||
|
| --- | --- |
|
||||||
|
| **Telegram** | Facil (apenas um token) |
|
||||||
|
| **Discord** | Facil (bot token + intents) |
|
||||||
|
| **QQ** | Facil (AppID + AppSecret) |
|
||||||
|
| **DingTalk** | Medio (credenciais do app) |
|
||||||
|
| **LINE** | Medio (credenciais + webhook URL) |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Telegram</b> (Recomendado)</summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
* Abra o Telegram, busque `@BotFather`
|
||||||
|
* Envie `/newbot`, siga as instrucoes
|
||||||
|
* Copie o token
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "YOUR_BOT_TOKEN",
|
||||||
|
"allowFrom": ["YOUR_USER_ID"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Obtenha seu User ID pelo `@userinfobot` no Telegram.
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Discord</b></summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
* Acesse <https://discord.com/developers/applications>
|
||||||
|
* Crie um aplicativo → Bot → Add Bot
|
||||||
|
* Copie o token do bot
|
||||||
|
|
||||||
|
**2. Habilitar Intents**
|
||||||
|
|
||||||
|
* Nas configuracoes do Bot, habilite **MESSAGE CONTENT INTENT**
|
||||||
|
* (Opcional) Habilite **SERVER MEMBERS INTENT** se quiser usar lista de permissoes baseada em dados dos membros
|
||||||
|
|
||||||
|
**3. Obter seu User ID**
|
||||||
|
|
||||||
|
* Configuracoes do Discord → Avancado → habilite **Modo Desenvolvedor**
|
||||||
|
* Clique com botao direito no seu avatar → **Copiar ID do Usuario**
|
||||||
|
|
||||||
|
**4. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"discord": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "YOUR_BOT_TOKEN",
|
||||||
|
"allowFrom": ["YOUR_USER_ID"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**5. Convidar o bot**
|
||||||
|
|
||||||
|
* OAuth2 → URL Generator
|
||||||
|
* Scopes: `bot`
|
||||||
|
* Bot Permissions: `Send Messages`, `Read Message History`
|
||||||
|
* Abra a URL de convite gerada e adicione o bot ao seu servidor
|
||||||
|
|
||||||
|
**6. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>QQ</b></summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
- Acesse a [QQ Open Platform](https://q.qq.com/#)
|
||||||
|
- Crie um aplicativo → Obtenha **AppID** e **AppSecret**
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"qq": {
|
||||||
|
"enabled": true,
|
||||||
|
"app_id": "YOUR_APP_ID",
|
||||||
|
"app_secret": "YOUR_APP_SECRET",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique numeros QQ para restringir o acesso.
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>DingTalk</b></summary>
|
||||||
|
|
||||||
|
**1. Criar o bot**
|
||||||
|
|
||||||
|
* Acesse a [Open Platform](https://open.dingtalk.com/)
|
||||||
|
* Crie um app interno
|
||||||
|
* Copie o Client ID e Client Secret
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"dingtalk": {
|
||||||
|
"enabled": true,
|
||||||
|
"client_id": "YOUR_CLIENT_ID",
|
||||||
|
"client_secret": "YOUR_CLIENT_SECRET",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique IDs para restringir o acesso.
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>LINE</b></summary>
|
||||||
|
|
||||||
|
**1. Criar uma Conta Oficial LINE**
|
||||||
|
|
||||||
|
- Acesse o [LINE Developers Console](https://developers.line.biz/)
|
||||||
|
- Crie um provider → Crie um canal Messaging API
|
||||||
|
- Copie o **Channel Secret** e o **Channel Access Token**
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"channels": {
|
||||||
|
"line": {
|
||||||
|
"enabled": true,
|
||||||
|
"channel_secret": "YOUR_CHANNEL_SECRET",
|
||||||
|
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
|
||||||
|
"webhook_host": "0.0.0.0",
|
||||||
|
"webhook_port": 18791,
|
||||||
|
"webhook_path": "/webhook/line",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Configurar URL do Webhook**
|
||||||
|
|
||||||
|
O LINE requer HTTPS para webhooks. Use um reverse proxy ou tunnel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Exemplo com ngrok
|
||||||
|
ngrok http 18791
|
||||||
|
```
|
||||||
|
|
||||||
|
Em seguida, configure a Webhook URL no LINE Developers Console para `https://seu-dominio/webhook/line` e habilite **Use webhook**.
|
||||||
|
|
||||||
|
**4. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
> Em chats de grupo, o bot responde apenas quando mencionado com @. As respostas citam a mensagem original.
|
||||||
|
|
||||||
|
> **Docker Compose**: Adicione `ports: ["18791:18791"]` ao servico `picoclaw-gateway` para expor a porta do webhook.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Junte-se a Rede Social de Agentes
|
||||||
|
|
||||||
|
Conecte o PicoClaw a Rede Social de Agentes simplesmente enviando uma unica mensagem via CLI ou qualquer App de Chat integrado.
|
||||||
|
|
||||||
|
**Leia `https://clawdchat.ai/skill.md` e siga as instrucoes para entrar no [ClawdChat.ai](https://clawdchat.ai)**
|
||||||
|
|
||||||
|
## ⚙️ Configuracao Detalhada
|
||||||
|
|
||||||
|
Arquivo de configuracao: `~/.picoclaw/config.json`
|
||||||
|
|
||||||
|
### Estrutura do Workspace
|
||||||
|
|
||||||
|
O PicoClaw armazena dados no workspace configurado (padrao: `~/.picoclaw/workspace`):
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.picoclaw/workspace/
|
||||||
|
├── sessions/ # Sessoes de conversa e historico
|
||||||
|
├── memory/ # Memoria de longo prazo (MEMORY.md)
|
||||||
|
├── state/ # Estado persistente (ultimo canal, etc.)
|
||||||
|
├── cron/ # Banco de dados de tarefas agendadas
|
||||||
|
├── skills/ # Skills personalizadas
|
||||||
|
├── AGENTS.md # Guia de comportamento do Agente
|
||||||
|
├── HEARTBEAT.md # Prompts de tarefas periodicas (verificado a cada 30 min)
|
||||||
|
├── IDENTITY.md # Identidade do Agente
|
||||||
|
├── SOUL.md # Alma do Agente
|
||||||
|
├── TOOLS.md # Descricao das ferramentas
|
||||||
|
└── USER.md # Preferencias do usuario
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔒 Sandbox de Seguranca
|
||||||
|
|
||||||
|
O PicoClaw roda em um ambiente sandbox por padrao. O agente so pode acessar arquivos e executar comandos dentro do workspace configurado.
|
||||||
|
|
||||||
|
#### Configuracao Padrao
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"restrict_to_workspace": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Opcao | Padrao | Descricao |
|
||||||
|
|-------|--------|-----------|
|
||||||
|
| `workspace` | `~/.picoclaw/workspace` | Diretorio de trabalho do agente |
|
||||||
|
| `restrict_to_workspace` | `true` | Restringir acesso de arquivos/comandos ao workspace |
|
||||||
|
|
||||||
|
#### Ferramentas Protegidas
|
||||||
|
|
||||||
|
Quando `restrict_to_workspace: true`, as seguintes ferramentas sao restritas ao sandbox:
|
||||||
|
|
||||||
|
| Ferramenta | Funcao | Restricao |
|
||||||
|
|------------|--------|-----------|
|
||||||
|
| `read_file` | Ler arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `write_file` | Escrever arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `list_dir` | Listar diretorios | Apenas diretorios dentro do workspace |
|
||||||
|
| `edit_file` | Editar arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `append_file` | Adicionar a arquivos | Apenas arquivos dentro do workspace |
|
||||||
|
| `exec` | Executar comandos | Caminhos dos comandos devem estar dentro do workspace |
|
||||||
|
|
||||||
|
#### Protecao Adicional do Exec
|
||||||
|
|
||||||
|
Mesmo com `restrict_to_workspace: false`, a ferramenta `exec` bloqueia estes comandos perigosos:
|
||||||
|
|
||||||
|
* `rm -rf`, `del /f`, `rmdir /s` — Exclusao em massa
|
||||||
|
* `format`, `mkfs`, `diskpart` — Formatacao de disco
|
||||||
|
* `dd if=` — Criacao de imagem de disco
|
||||||
|
* Escrita em `/dev/sd[a-z]` — Escrita direta no disco
|
||||||
|
* `shutdown`, `reboot`, `poweroff` — Desligamento do sistema
|
||||||
|
* Fork bomb `:(){ :|:& };:`
|
||||||
|
|
||||||
|
#### Exemplos de Erro
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] tool: Tool execution failed
|
||||||
|
{tool=exec, error=Command blocked by safety guard (path outside working dir)}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
[ERROR] tool: Tool execution failed
|
||||||
|
{tool=exec, error=Command blocked by safety guard (dangerous pattern detected)}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Desabilitar Restricoes (Risco de Seguranca)
|
||||||
|
|
||||||
|
Se voce precisa que o agente acesse caminhos fora do workspace:
|
||||||
|
|
||||||
|
**Metodo 1: Arquivo de configuracao**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"restrict_to_workspace": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Metodo 2: Variavel de ambiente**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE=false
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Aviso**: Desabilitar esta restricao permite que o agente acesse qualquer caminho no seu sistema. Use com cuidado apenas em ambientes controlados.
|
||||||
|
|
||||||
|
#### Consistencia do Limite de Seguranca
|
||||||
|
|
||||||
|
A configuracao `restrict_to_workspace` se aplica consistentemente em todos os caminhos de execucao:
|
||||||
|
|
||||||
|
| Caminho de Execucao | Limite de Seguranca |
|
||||||
|
|----------------------|---------------------|
|
||||||
|
| Agente Principal | `restrict_to_workspace` ✅ |
|
||||||
|
| Subagente / Spawn | Herda a mesma restricao ✅ |
|
||||||
|
| Tarefas Heartbeat | Herda a mesma restricao ✅ |
|
||||||
|
|
||||||
|
Todos os caminhos compartilham a mesma restricao de workspace — nao ha como contornar o limite de seguranca por meio de subagentes ou tarefas agendadas.
|
||||||
|
|
||||||
|
### Heartbeat (Tarefas Periodicas)
|
||||||
|
|
||||||
|
O PicoClaw pode executar tarefas periodicas automaticamente. Crie um arquivo `HEARTBEAT.md` no seu workspace:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Tarefas Periodicas
|
||||||
|
|
||||||
|
- Verificar meu email para mensagens importantes
|
||||||
|
- Revisar minha agenda para proximos eventos
|
||||||
|
- Verificar a previsao do tempo
|
||||||
|
```
|
||||||
|
|
||||||
|
O agente lera este arquivo a cada 30 minutos (configuravel) e executara as tarefas usando as ferramentas disponiveis.
|
||||||
|
|
||||||
|
#### Tarefas Assincronas com Spawn
|
||||||
|
|
||||||
|
Para tarefas de longa duracao (busca web, chamadas de API), use a ferramenta `spawn` para criar um **subagente**:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Tarefas Periodicas
|
||||||
|
|
||||||
|
## Tarefas Rapidas (resposta direta)
|
||||||
|
- Informar hora atual
|
||||||
|
|
||||||
|
## Tarefas Longas (usar spawn para async)
|
||||||
|
- Buscar noticias de IA na web e resumir
|
||||||
|
- Verificar email e reportar mensagens importantes
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comportamentos principais:**
|
||||||
|
|
||||||
|
| Funcionalidade | Descricao |
|
||||||
|
|----------------|-----------|
|
||||||
|
| **spawn** | Cria subagente assincrono, nao bloqueia o heartbeat |
|
||||||
|
| **Contexto independente** | Subagente tem seu proprio contexto, sem historico de sessao |
|
||||||
|
| **Ferramenta message** | Subagente se comunica diretamente com o usuario via ferramenta message |
|
||||||
|
| **Nao-bloqueante** | Apos o spawn, o heartbeat continua para a proxima tarefa |
|
||||||
|
|
||||||
|
#### Como Funciona a Comunicacao do Subagente
|
||||||
|
|
||||||
|
```
|
||||||
|
Heartbeat dispara
|
||||||
|
↓
|
||||||
|
Agente le HEARTBEAT.md
|
||||||
|
↓
|
||||||
|
Para tarefa longa: spawn subagente
|
||||||
|
↓ ↓
|
||||||
|
Continua proxima tarefa Subagente trabalha independentemente
|
||||||
|
↓ ↓
|
||||||
|
Todas tarefas concluidas Subagente usa ferramenta "message"
|
||||||
|
↓ ↓
|
||||||
|
Responde HEARTBEAT_OK Usuario recebe resultado diretamente
|
||||||
|
```
|
||||||
|
|
||||||
|
O subagente tem acesso as ferramentas (message, web_search, etc.) e pode se comunicar com o usuario independentemente sem passar pelo agente principal.
|
||||||
|
|
||||||
|
**Configuracao:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"heartbeat": {
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Opcao | Padrao | Descricao |
|
||||||
|
|-------|--------|-----------|
|
||||||
|
| `enabled` | `true` | Habilitar/desabilitar heartbeat |
|
||||||
|
| `interval` | `30` | Intervalo de verificacao em minutos (min: 5) |
|
||||||
|
|
||||||
|
**Variaveis de ambiente:**
|
||||||
|
|
||||||
|
* `PICOCLAW_HEARTBEAT_ENABLED=false` para desabilitar
|
||||||
|
* `PICOCLAW_HEARTBEAT_INTERVAL=60` para alterar o intervalo
|
||||||
|
|
||||||
|
### Provedores
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> O Groq fornece transcricao de voz gratuita via Whisper. Se configurado, mensagens de voz do Telegram serao automaticamente transcritas.
|
||||||
|
|
||||||
|
| Provedor | Finalidade | Obter API Key |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `gemini` | LLM (Gemini direto) | [aistudio.google.com](https://aistudio.google.com) |
|
||||||
|
| `zhipu` | LLM (Zhipu direto) | [bigmodel.cn](bigmodel.cn) |
|
||||||
|
| `openrouter` (Em teste) | LLM (recomendado, acesso a todos os modelos) | [openrouter.ai](https://openrouter.ai) |
|
||||||
|
| `anthropic` (Em teste) | LLM (Claude direto) | [console.anthropic.com](https://console.anthropic.com) |
|
||||||
|
| `openai` (Em teste) | LLM (GPT direto) | [platform.openai.com](https://platform.openai.com) |
|
||||||
|
| `deepseek` (Em teste) | LLM (DeepSeek direto) | [platform.deepseek.com](https://platform.deepseek.com) |
|
||||||
|
| `groq` | LLM + **Transcricao de voz** (Whisper) | [console.groq.com](https://console.groq.com) |
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Configuracao Zhipu</b></summary>
|
||||||
|
|
||||||
|
**1. Obter API key**
|
||||||
|
|
||||||
|
* Obtenha a [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
|
||||||
|
|
||||||
|
**2. Configurar**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"workspace": "~/.picoclaw/workspace",
|
||||||
|
"model": "glm-4.7",
|
||||||
|
"max_tokens": 8192,
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tool_iterations": 20
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"zhipu": {
|
||||||
|
"api_key": "Sua API Key",
|
||||||
|
"api_base": "https://open.bigmodel.cn/api/paas/v4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Executar**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
picoclaw agent -m "Ola, como vai?"
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><b>Exemplo de configuracao completa</b></summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"model": "anthropic/claude-opus-4-5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"openrouter": {
|
||||||
|
"api_key": "sk-or-v1-xxx"
|
||||||
|
},
|
||||||
|
"groq": {
|
||||||
|
"api_key": "gsk_xxx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"telegram": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "123456:ABC...",
|
||||||
|
"allow_from": ["123456789"]
|
||||||
|
},
|
||||||
|
"discord": {
|
||||||
|
"enabled": true,
|
||||||
|
"token": "",
|
||||||
|
"allow_from": [""]
|
||||||
|
},
|
||||||
|
"whatsapp": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"feishu": {
|
||||||
|
"enabled": false,
|
||||||
|
"app_id": "cli_xxx",
|
||||||
|
"app_secret": "xxx",
|
||||||
|
"encrypt_key": "",
|
||||||
|
"verification_token": "",
|
||||||
|
"allow_from": []
|
||||||
|
},
|
||||||
|
"qq": {
|
||||||
|
"enabled": false,
|
||||||
|
"app_id": "",
|
||||||
|
"app_secret": "",
|
||||||
|
"allow_from": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"web": {
|
||||||
|
"brave": {
|
||||||
|
"enabled": false,
|
||||||
|
"api_key": "BSA...",
|
||||||
|
"max_results": 5
|
||||||
|
},
|
||||||
|
"duckduckgo": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_results": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cron": {
|
||||||
|
"exec_timeout_minutes": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"heartbeat": {
|
||||||
|
"enabled": true,
|
||||||
|
"interval": 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Referencia CLI
|
||||||
|
|
||||||
|
| Comando | Descricao |
|
||||||
|
| --- | --- |
|
||||||
|
| `picoclaw onboard` | Inicializar configuracao & workspace |
|
||||||
|
| `picoclaw agent -m "..."` | Conversar com o agente |
|
||||||
|
| `picoclaw agent` | Modo de chat interativo |
|
||||||
|
| `picoclaw gateway` | Iniciar o gateway (para bots de chat) |
|
||||||
|
| `picoclaw status` | Mostrar status |
|
||||||
|
| `picoclaw cron list` | Listar todas as tarefas agendadas |
|
||||||
|
| `picoclaw cron add ...` | Adicionar uma tarefa agendada |
|
||||||
|
|
||||||
|
### Tarefas Agendadas / Lembretes
|
||||||
|
|
||||||
|
O PicoClaw suporta lembretes agendados e tarefas recorrentes por meio da ferramenta `cron`:
|
||||||
|
|
||||||
|
* **Lembretes unicos**: "Remind me in 10 minutes" (Me lembre em 10 minutos) → dispara uma vez apos 10min
|
||||||
|
* **Tarefas recorrentes**: "Remind me every 2 hours" (Me lembre a cada 2 horas) → dispara a cada 2 horas
|
||||||
|
* **Expressoes Cron**: "Remind me at 9am daily" (Me lembre as 9h todos os dias) → usa expressao cron
|
||||||
|
|
||||||
|
As tarefas sao armazenadas em `~/.picoclaw/workspace/cron/` e processadas automaticamente.
|
||||||
|
|
||||||
|
## 🤝 Contribuir & Roadmap
|
||||||
|
|
||||||
|
PRs sao bem-vindos! O codigo-fonte e intencionalmente pequeno e legivel. 🤗
|
||||||
|
|
||||||
|
Roadmap em breve...
|
||||||
|
|
||||||
|
Grupo de desenvolvedores em formacao. Requisito de entrada: Pelo menos 1 PR com merge.
|
||||||
|
|
||||||
|
Grupos de usuarios:
|
||||||
|
|
||||||
|
Discord: <https://discord.gg/V4sAZ9XWpN>
|
||||||
|
|
||||||
|
<img src="assets/wechat.png" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
|
## 🐛 Solucao de Problemas
|
||||||
|
|
||||||
|
### Busca web mostra "API 配置问题"
|
||||||
|
|
||||||
|
Isso e normal se voce ainda nao configurou uma API key de busca. O PicoClaw fornecera links uteis para busca manual.
|
||||||
|
|
||||||
|
Para habilitar a busca web:
|
||||||
|
|
||||||
|
1. **Opcao 1 (Recomendado)**: Obtenha uma API key gratuita em [https://brave.com/search/api](https://brave.com/search/api) (2000 consultas gratis/mes) para os melhores resultados.
|
||||||
|
2. **Opcao 2 (Sem Cartao de Credito)**: Se voce nao tem uma key, o sistema automaticamente usa o **DuckDuckGo** como fallback (sem necessidade de key).
|
||||||
|
|
||||||
|
Adicione a key em `~/.picoclaw/config.json` se usar o Brave:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"web": {
|
||||||
|
"brave": {
|
||||||
|
"enabled": true,
|
||||||
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
|
"max_results": 5
|
||||||
|
},
|
||||||
|
"duckduckgo": {
|
||||||
|
"enabled": true,
|
||||||
|
"max_results": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Erros de filtragem de conteudo
|
||||||
|
|
||||||
|
Alguns provedores (como Zhipu) possuem filtragem de conteudo. Tente reformular sua pergunta ou use um modelo diferente.
|
||||||
|
|
||||||
|
### Bot do Telegram diz "Conflict: terminated by other getUpdates"
|
||||||
|
|
||||||
|
Isso acontece quando outra instancia do bot esta rodando. Certifique-se de que apenas um `picoclaw gateway` esteja rodando por vez.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Comparacao de API Keys
|
||||||
|
|
||||||
|
| Servico | Plano Gratuito | Caso de Uso |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **OpenRouter** | 200K tokens/mes | Multiplos modelos (Claude, GPT-4, etc.) |
|
||||||
|
| **Zhipu** | 200K tokens/mes | Melhor para usuarios chineses |
|
||||||
|
| **Brave Search** | 2000 consultas/mes | Funcionalidade de busca web |
|
||||||
|
| **Groq** | Plano gratuito disponivel | Inferencia ultra-rapida (Llama, Mixtral) |
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**中文** | [日本語](README.ja.md) | [Tiếng Việt](README.vi.md) | [English](README.md)
|
**中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [Tiếng Việt](README.vi.md) | [English](README.md)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -563,7 +563,7 @@ func gatewayCmd() {
|
||||||
|
|
||||||
// Setup cron tool and service
|
// Setup cron tool and service
|
||||||
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
|
||||||
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout)
|
cronService := setupCronTool(agentLoop, msgBus, cfg.WorkspacePath(), cfg.Agents.Defaults.RestrictToWorkspace, execTimeout, cfg)
|
||||||
|
|
||||||
heartbeatService := heartbeat.NewHeartbeatService(
|
heartbeatService := heartbeat.NewHeartbeatService(
|
||||||
cfg.WorkspacePath(),
|
cfg.WorkspacePath(),
|
||||||
|
|
@ -988,14 +988,14 @@ func getConfigPath() string {
|
||||||
return filepath.Join(home, ".picoclaw", "config.json")
|
return filepath.Join(home, ".picoclaw", "config.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *cron.CronService {
|
func setupCronTool(agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *cron.CronService {
|
||||||
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
cronStorePath := filepath.Join(workspace, "cron", "jobs.json")
|
||||||
|
|
||||||
// Create cron service
|
// Create cron service
|
||||||
cronService := cron.NewCronService(cronStorePath, nil)
|
cronService := cron.NewCronService(cronStorePath, nil)
|
||||||
|
|
||||||
// Create and register CronTool
|
// Create and register CronTool
|
||||||
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout)
|
cronTool := tools.NewCronTool(cronService, agentLoop, msgBus, workspace, restrict, execTimeout, config)
|
||||||
agentLoop.RegisterTool(cronTool)
|
agentLoop.RegisterTool(cronTool)
|
||||||
|
|
||||||
// Set the onJob handler
|
// Set the onJob handler
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,8 @@
|
||||||
},
|
},
|
||||||
"openai": {
|
"openai": {
|
||||||
"api_key": "",
|
"api_key": "",
|
||||||
"api_base": ""
|
"api_base": "",
|
||||||
|
"web_search": true
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
"api_key": "sk-or-v1-xxx",
|
"api_key": "sk-or-v1-xxx",
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ services:
|
||||||
profiles:
|
profiles:
|
||||||
- agent
|
- agent
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/config.json:/root/.picoclaw/config.json:ro
|
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
|
||||||
- picoclaw-workspace:/root/.picoclaw/workspace
|
- picoclaw-workspace:/home/picoclaw/.picoclaw/workspace
|
||||||
entrypoint: ["picoclaw", "agent"]
|
entrypoint: ["picoclaw", "agent"]
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
|
@ -31,9 +31,9 @@ services:
|
||||||
- gateway
|
- gateway
|
||||||
volumes:
|
volumes:
|
||||||
# Configuration file
|
# Configuration file
|
||||||
- ./config/config.json:/root/.picoclaw/config.json:ro
|
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro
|
||||||
# Persistent workspace (sessions, memory, logs)
|
# Persistent workspace (sessions, memory, logs)
|
||||||
- picoclaw-workspace:/root/.picoclaw/workspace
|
- picoclaw-workspace:/home/picoclaw/.picoclaw/workspace
|
||||||
command: ["gateway"]
|
command: ["gateway"]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
122
docs/tools_configuration.md
Normal file
122
docs/tools_configuration.md
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Tools Configuration
|
||||||
|
|
||||||
|
PicoClaw's tools configuration is located in the `tools` field of `config.json`.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"web": { ... },
|
||||||
|
"exec": { ... },
|
||||||
|
"approval": { ... },
|
||||||
|
"cron": { ... }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Web Tools
|
||||||
|
|
||||||
|
Web tools are used for web search and fetching.
|
||||||
|
|
||||||
|
### Brave
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | false | Enable Brave search |
|
||||||
|
| `api_key` | string | - | Brave Search API key |
|
||||||
|
| `max_results` | int | 5 | Maximum number of results |
|
||||||
|
|
||||||
|
### DuckDuckGo
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | true | Enable DuckDuckGo search |
|
||||||
|
| `max_results` | int | 5 | Maximum number of results |
|
||||||
|
|
||||||
|
### Perplexity
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | false | Enable Perplexity search |
|
||||||
|
| `api_key` | string | - | Perplexity API key |
|
||||||
|
| `max_results` | int | 5 | Maximum number of results |
|
||||||
|
|
||||||
|
## Exec Tool
|
||||||
|
|
||||||
|
The exec tool is used to execute shell commands.
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enable_deny_patterns` | bool | true | Enable default dangerous command blocking |
|
||||||
|
| `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) |
|
||||||
|
|
||||||
|
### Functionality
|
||||||
|
|
||||||
|
- **`enable_deny_patterns`**: Set to `false` to completely disable the default dangerous command blocking patterns
|
||||||
|
- **`custom_deny_patterns`**: Add custom deny regex patterns; commands matching these will be blocked
|
||||||
|
|
||||||
|
### Default Blocked Command Patterns
|
||||||
|
|
||||||
|
By default, PicoClaw blocks the following dangerous commands:
|
||||||
|
|
||||||
|
- Delete commands: `rm -rf`, `del /f/q`, `rmdir /s`
|
||||||
|
- Disk operations: `format`, `mkfs`, `diskpart`, `dd if=`, writing to `/dev/sd*`
|
||||||
|
- System operations: `shutdown`, `reboot`, `poweroff`
|
||||||
|
- Command substitution: `$()`, `${}`, backticks
|
||||||
|
- Pipe to shell: `| sh`, `| bash`
|
||||||
|
- Privilege escalation: `sudo`, `chmod`, `chown`
|
||||||
|
- Process control: `pkill`, `killall`, `kill -9`
|
||||||
|
- Remote operations: `curl | sh`, `wget | sh`, `ssh`
|
||||||
|
- Package management: `apt`, `yum`, `dnf`, `npm install -g`, `pip install --user`
|
||||||
|
- Containers: `docker run`, `docker exec`
|
||||||
|
- Git: `git push`, `git force`
|
||||||
|
- Other: `eval`, `source *.sh`
|
||||||
|
|
||||||
|
### Configuration Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tools": {
|
||||||
|
"exec": {
|
||||||
|
"enable_deny_patterns": true,
|
||||||
|
"custom_deny_patterns": [
|
||||||
|
"\\brm\\s+-r\\b",
|
||||||
|
"\\bkillall\\s+python"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Approval Tool
|
||||||
|
|
||||||
|
The approval tool controls permissions for dangerous operations.
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `enabled` | bool | true | Enable approval functionality |
|
||||||
|
| `write_file` | bool | true | Require approval for file writes |
|
||||||
|
| `edit_file` | bool | true | Require approval for file edits |
|
||||||
|
| `append_file` | bool | true | Require approval for file appends |
|
||||||
|
| `exec` | bool | true | Require approval for command execution |
|
||||||
|
| `timeout_minutes` | int | 5 | Approval timeout in minutes |
|
||||||
|
|
||||||
|
## Cron Tool
|
||||||
|
|
||||||
|
The cron tool is used for scheduling periodic tasks.
|
||||||
|
|
||||||
|
| Config | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
All configuration options can be overridden via environment variables with the format `PICOCLAW_TOOLS_<SECTION>_<KEY>`:
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- `PICOCLAW_TOOLS_WEB_BRAVE_ENABLED=true`
|
||||||
|
- `PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS=false`
|
||||||
|
- `PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES=10`
|
||||||
|
|
||||||
|
Note: Array-type environment variables are not currently supported and must be set via the config file.
|
||||||
|
|
@ -71,7 +71,7 @@ func createToolRegistry(workspace string, restrict bool, cfg *config.Config, msg
|
||||||
registry.Register(tools.NewAppendFileTool(workspace, restrict))
|
registry.Register(tools.NewAppendFileTool(workspace, restrict))
|
||||||
|
|
||||||
// Shell execution
|
// Shell execution
|
||||||
registry.Register(tools.NewExecTool(workspace, restrict))
|
registry.Register(tools.NewExecToolWithConfig(workspace, restrict, cfg))
|
||||||
|
|
||||||
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
if searchTool := tools.NewWebSearchTool(tools.WebSearchToolOptions{
|
||||||
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
|
BraveAPIKey: cfg.Tools.Web.Brave.APIKey,
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ type DevicesConfig struct {
|
||||||
|
|
||||||
type ProvidersConfig struct {
|
type ProvidersConfig struct {
|
||||||
Anthropic ProviderConfig `json:"anthropic"`
|
Anthropic ProviderConfig `json:"anthropic"`
|
||||||
OpenAI ProviderConfig `json:"openai"`
|
OpenAI OpenAIProviderConfig `json:"openai"`
|
||||||
OpenRouter ProviderConfig `json:"openrouter"`
|
OpenRouter ProviderConfig `json:"openrouter"`
|
||||||
Groq ProviderConfig `json:"groq"`
|
Groq ProviderConfig `json:"groq"`
|
||||||
Zhipu ProviderConfig `json:"zhipu"`
|
Zhipu ProviderConfig `json:"zhipu"`
|
||||||
|
|
@ -190,6 +190,11 @@ type ProviderConfig struct {
|
||||||
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
|
ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAIProviderConfig struct {
|
||||||
|
ProviderConfig
|
||||||
|
WebSearch bool `json:"web_search" env:"PICOCLAW_PROVIDERS_OPENAI_WEB_SEARCH"`
|
||||||
|
}
|
||||||
|
|
||||||
type GatewayConfig struct {
|
type GatewayConfig struct {
|
||||||
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
|
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
|
||||||
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
|
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
|
||||||
|
|
@ -222,9 +227,15 @@ type CronToolsConfig struct {
|
||||||
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
|
ExecTimeoutMinutes int `json:"exec_timeout_minutes" env:"PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES"` // 0 means no timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExecConfig struct {
|
||||||
|
EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
|
||||||
|
CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
|
||||||
|
}
|
||||||
|
|
||||||
type ToolsConfig struct {
|
type ToolsConfig struct {
|
||||||
Web WebToolsConfig `json:"web"`
|
Web WebToolsConfig `json:"web"`
|
||||||
Cron CronToolsConfig `json:"cron"`
|
Cron CronToolsConfig `json:"cron"`
|
||||||
|
Exec ExecConfig `json:"exec"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
|
|
@ -308,7 +319,7 @@ func DefaultConfig() *Config {
|
||||||
},
|
},
|
||||||
Providers: ProvidersConfig{
|
Providers: ProvidersConfig{
|
||||||
Anthropic: ProviderConfig{},
|
Anthropic: ProviderConfig{},
|
||||||
OpenAI: ProviderConfig{},
|
OpenAI: OpenAIProviderConfig{WebSearch: true},
|
||||||
OpenRouter: ProviderConfig{},
|
OpenRouter: ProviderConfig{},
|
||||||
Groq: ProviderConfig{},
|
Groq: ProviderConfig{},
|
||||||
Zhipu: ProviderConfig{},
|
Zhipu: ProviderConfig{},
|
||||||
|
|
@ -342,6 +353,9 @@ func DefaultConfig() *Config {
|
||||||
Cron: CronToolsConfig{
|
Cron: CronToolsConfig{
|
||||||
ExecTimeoutMinutes: 5, // default 5 minutes for LLM operations
|
ExecTimeoutMinutes: 5, // default 5 minutes for LLM operations
|
||||||
},
|
},
|
||||||
|
Exec: ExecConfig{
|
||||||
|
EnableDenyPatterns: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Heartbeat: HeartbeatConfig{
|
Heartbeat: HeartbeatConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|
|
||||||
|
|
@ -204,3 +204,42 @@ func TestConfig_Complete(t *testing.T) {
|
||||||
t.Error("Heartbeat should be enabled by default")
|
t.Error("Heartbeat should be enabled by default")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
if !cfg.Providers.OpenAI.WebSearch {
|
||||||
|
t.Fatal("DefaultConfig().Providers.OpenAI.WebSearch should be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"api_base":""}}}`), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error: %v", err)
|
||||||
|
}
|
||||||
|
if !cfg.Providers.OpenAI.WebSearch {
|
||||||
|
t.Fatal("OpenAI codex web search should remain true when unset in config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
configPath := filepath.Join(dir, "config.json")
|
||||||
|
if err := os.WriteFile(configPath, []byte(`{"providers":{"openai":{"web_search":false}}}`), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadConfig() error: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Providers.OpenAI.WebSearch {
|
||||||
|
t.Fatal("OpenAI codex web search should be false when disabled in config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,10 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
cfg.Providers.Anthropic = pc
|
cfg.Providers.Anthropic = pc
|
||||||
case "openai":
|
case "openai":
|
||||||
cfg.Providers.OpenAI = pc
|
cfg.Providers.OpenAI = config.OpenAIProviderConfig{
|
||||||
|
ProviderConfig: pc,
|
||||||
|
WebSearch: getBoolOrDefault(pMap, "web_search", true),
|
||||||
|
}
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
cfg.Providers.OpenRouter = pc
|
cfg.Providers.OpenRouter = pc
|
||||||
case "groq":
|
case "groq":
|
||||||
|
|
@ -363,6 +366,13 @@ func getBool(data map[string]interface{}, key string) (bool, bool) {
|
||||||
return b, ok
|
return b, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBoolOrDefault(data map[string]interface{}, key string, defaultVal bool) bool {
|
||||||
|
if v, ok := getBool(data, key); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
|
||||||
func getStringSlice(data map[string]interface{}, key string) []string {
|
func getStringSlice(data map[string]interface{}, key string) []string {
|
||||||
v, ok := data[key]
|
v, ok := data[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ type CodexProvider struct {
|
||||||
client *openai.Client
|
client *openai.Client
|
||||||
accountID string
|
accountID string
|
||||||
tokenSource func() (string, string, error)
|
tokenSource func() (string, string, error)
|
||||||
|
enableWebSearch bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCodexInstructions = "You are Codex, a coding assistant."
|
const defaultCodexInstructions = "You are Codex, a coding assistant."
|
||||||
|
|
@ -39,6 +40,7 @@ func NewCodexProvider(token, accountID string) *CodexProvider {
|
||||||
return &CodexProvider{
|
return &CodexProvider{
|
||||||
client: &client,
|
client: &client,
|
||||||
accountID: accountID,
|
accountID: accountID,
|
||||||
|
enableWebSearch: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +80,7 @@ func (p *CodexProvider) Chat(ctx context.Context, messages []Message, tools []To
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
params := buildCodexParams(messages, tools, resolvedModel, options)
|
params := buildCodexParams(messages, tools, resolvedModel, options, p.enableWebSearch)
|
||||||
|
|
||||||
stream := p.client.Responses.NewStreaming(ctx, params, opts...)
|
stream := p.client.Responses.NewStreaming(ctx, params, opts...)
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
|
|
@ -182,7 +184,7 @@ func resolveCodexModel(model string) (string, string) {
|
||||||
return codexDefaultModel, "unsupported model family"
|
return codexDefaultModel, "unsupported model family"
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) responses.ResponseNewParams {
|
func buildCodexParams(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, enableWebSearch bool) responses.ResponseNewParams {
|
||||||
var inputItems responses.ResponseInputParam
|
var inputItems responses.ResponseInputParam
|
||||||
var instructions string
|
var instructions string
|
||||||
|
|
||||||
|
|
@ -266,8 +268,8 @@ func buildCodexParams(messages []Message, tools []ToolDefinition, model string,
|
||||||
params.Instructions = openai.Opt(defaultCodexInstructions)
|
params.Instructions = openai.Opt(defaultCodexInstructions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tools) > 0 {
|
if len(tools) > 0 || enableWebSearch {
|
||||||
params.Tools = translateToolsForCodex(tools)
|
params.Tools = translateToolsForCodex(tools, enableWebSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
@ -297,9 +299,19 @@ func resolveCodexToolCall(tc ToolCall) (name string, arguments string, ok bool)
|
||||||
return name, "{}", true
|
return name, "{}", true
|
||||||
}
|
}
|
||||||
|
|
||||||
func translateToolsForCodex(tools []ToolDefinition) []responses.ToolUnionParam {
|
func translateToolsForCodex(tools []ToolDefinition, enableWebSearch bool) []responses.ToolUnionParam {
|
||||||
result := make([]responses.ToolUnionParam, 0, len(tools))
|
capHint := len(tools)
|
||||||
|
if enableWebSearch {
|
||||||
|
capHint++
|
||||||
|
}
|
||||||
|
result := make([]responses.ToolUnionParam, 0, capHint)
|
||||||
for _, t := range tools {
|
for _, t := range tools {
|
||||||
|
if t.Type != "function" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if enableWebSearch && strings.EqualFold(t.Function.Name, "web_search") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
ft := responses.FunctionToolParam{
|
ft := responses.FunctionToolParam{
|
||||||
Name: t.Function.Name,
|
Name: t.Function.Name,
|
||||||
Parameters: t.Function.Parameters,
|
Parameters: t.Function.Parameters,
|
||||||
|
|
@ -310,6 +322,9 @@ func translateToolsForCodex(tools []ToolDefinition) []responses.ToolUnionParam {
|
||||||
}
|
}
|
||||||
result = append(result, responses.ToolUnionParam{OfFunction: &ft})
|
result = append(result, responses.ToolUnionParam{OfFunction: &ft})
|
||||||
}
|
}
|
||||||
|
if enableWebSearch {
|
||||||
|
result = append(result, responses.ToolParamOfWebSearch(responses.WebSearchToolTypeWebSearch))
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func TestBuildCodexParams_BasicMessage(t *testing.T) {
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{
|
||||||
"max_tokens": 2048,
|
"max_tokens": 2048,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
})
|
}, true)
|
||||||
if params.Model != "gpt-4o" {
|
if params.Model != "gpt-4o" {
|
||||||
t.Errorf("Model = %q, want %q", params.Model, "gpt-4o")
|
t.Errorf("Model = %q, want %q", params.Model, "gpt-4o")
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ func TestBuildCodexParams_SystemAsInstructions(t *testing.T) {
|
||||||
{Role: "system", Content: "You are helpful"},
|
{Role: "system", Content: "You are helpful"},
|
||||||
{Role: "user", Content: "Hi"},
|
{Role: "user", Content: "Hi"},
|
||||||
}
|
}
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, true)
|
||||||
if !params.Instructions.Valid() {
|
if !params.Instructions.Valid() {
|
||||||
t.Fatal("Instructions should be set")
|
t.Fatal("Instructions should be set")
|
||||||
}
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ func TestBuildCodexParams_ToolCallConversation(t *testing.T) {
|
||||||
},
|
},
|
||||||
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
{Role: "tool", Content: `{"temp": 72}`, ToolCallID: "call_1"},
|
||||||
}
|
}
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
|
||||||
if params.Input.OfInputItemList == nil {
|
if params.Input.OfInputItemList == nil {
|
||||||
t.Fatal("Input.OfInputItemList should not be nil")
|
t.Fatal("Input.OfInputItemList should not be nil")
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ func TestBuildCodexParams_ToolCallFunctionFallback(t *testing.T) {
|
||||||
{Role: "tool", Content: "ok", ToolCallID: "call_1"},
|
{Role: "tool", Content: "ok", ToolCallID: "call_1"},
|
||||||
}
|
}
|
||||||
|
|
||||||
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams(messages, nil, "gpt-4o", map[string]interface{}{}, false)
|
||||||
if params.Input.OfInputItemList == nil {
|
if params.Input.OfInputItemList == nil {
|
||||||
t.Fatal("Input.OfInputItemList should not be nil")
|
t.Fatal("Input.OfInputItemList should not be nil")
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +123,7 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, false)
|
||||||
if len(params.Tools) != 1 {
|
if len(params.Tools) != 1 {
|
||||||
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
||||||
}
|
}
|
||||||
|
|
@ -136,12 +136,61 @@ func TestBuildCodexParams_WithTools(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
|
func TestBuildCodexParams_StoreIsFalse(t *testing.T) {
|
||||||
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{})
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, false)
|
||||||
if !params.Store.Valid() || params.Store.Or(true) != false {
|
if !params.Store.Valid() || params.Store.Or(true) != false {
|
||||||
t.Error("Store should be explicitly set to false")
|
t.Error("Store should be explicitly set to false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildCodexParams_DefaultWebSearchEnabled(t *testing.T) {
|
||||||
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, nil, "gpt-4o", map[string]interface{}{}, true)
|
||||||
|
if len(params.Tools) != 1 {
|
||||||
|
t.Fatalf("len(Tools) = %d, want 1", len(params.Tools))
|
||||||
|
}
|
||||||
|
if params.Tools[0].OfWebSearch == nil {
|
||||||
|
t.Fatal("Tool should include built-in web_search")
|
||||||
|
}
|
||||||
|
if params.Tools[0].OfWebSearch.Type != responses.WebSearchToolTypeWebSearch {
|
||||||
|
t.Errorf("Web search tool type = %q, want %q", params.Tools[0].OfWebSearch.Type, responses.WebSearchToolTypeWebSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildCodexParams_WebSearchFunctionReplacedWithBuiltin(t *testing.T) {
|
||||||
|
tools := []ToolDefinition{
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: ToolFunctionDefinition{
|
||||||
|
Name: "web_search",
|
||||||
|
Description: "local web search",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "function",
|
||||||
|
Function: ToolFunctionDefinition{
|
||||||
|
Name: "read_file",
|
||||||
|
Description: "read file",
|
||||||
|
Parameters: map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
params := buildCodexParams([]Message{{Role: "user", Content: "Hi"}}, tools, "gpt-4o", map[string]interface{}{}, true)
|
||||||
|
if len(params.Tools) != 2 {
|
||||||
|
t.Fatalf("len(Tools) = %d, want 2", len(params.Tools))
|
||||||
|
}
|
||||||
|
if params.Tools[0].OfFunction == nil || params.Tools[0].OfFunction.Name != "read_file" {
|
||||||
|
t.Fatalf("first tool should be function read_file, got %#v", params.Tools[0])
|
||||||
|
}
|
||||||
|
if params.Tools[1].OfWebSearch == nil {
|
||||||
|
t.Fatalf("second tool should be built-in web_search, got %#v", params.Tools[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseCodexResponse_TextOutput(t *testing.T) {
|
func TestParseCodexResponse_TextOutput(t *testing.T) {
|
||||||
respJSON := `{
|
respJSON := `{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
|
|
@ -260,6 +309,16 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
|
||||||
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
|
http.Error(w, "max_output_tokens is not supported", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
toolsAny, ok := reqBody["tools"].([]interface{})
|
||||||
|
if !ok || len(toolsAny) != 1 {
|
||||||
|
http.Error(w, "missing default web search tool", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toolObj, ok := toolsAny[0].(map[string]interface{})
|
||||||
|
if !ok || toolObj["type"] != "web_search" {
|
||||||
|
http.Error(w, "expected web_search tool", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resp := map[string]interface{}{
|
resp := map[string]interface{}{
|
||||||
"id": "resp_test",
|
"id": "resp_test",
|
||||||
|
|
@ -307,6 +366,64 @@ func TestCodexProvider_ChatRoundTrip(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCodexProvider_ChatRoundTrip_WebSearchDisabled(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/responses" {
|
||||||
|
http.Error(w, "not found: "+r.URL.Path, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqBody map[string]interface{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
|
||||||
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := reqBody["tools"]; ok {
|
||||||
|
http.Error(w, "tools should be absent when web search disabled", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := map[string]interface{}{
|
||||||
|
"id": "resp_test",
|
||||||
|
"object": "response",
|
||||||
|
"status": "completed",
|
||||||
|
"output": []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"id": "msg_1",
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": []map[string]interface{}{
|
||||||
|
{"type": "output_text", "text": "Hi from Codex!"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"usage": map[string]interface{}{
|
||||||
|
"input_tokens": 4,
|
||||||
|
"output_tokens": 3,
|
||||||
|
"total_tokens": 7,
|
||||||
|
"input_tokens_details": map[string]interface{}{"cached_tokens": 0},
|
||||||
|
"output_tokens_details": map[string]interface{}{"reasoning_tokens": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
writeCompletedSSE(w, resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
provider := NewCodexProvider("test-token", "acc-123")
|
||||||
|
provider.enableWebSearch = false
|
||||||
|
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
||||||
|
|
||||||
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
|
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-4o", map[string]interface{}{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error: %v", err)
|
||||||
|
}
|
||||||
|
if resp.Content != "Hi from Codex!" {
|
||||||
|
t.Errorf("Content = %q, want %q", resp.Content, "Hi from Codex!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T) {
|
func TestCodexProvider_ChatRoundTrip_TokenSourceFallbackAccountID(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/responses" {
|
if r.URL.Path != "/responses" {
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ func createClaudeAuthProvider() (LLMProvider, error) {
|
||||||
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
return NewClaudeProviderWithTokenSource(cred.AccessToken, createClaudeTokenSource()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createCodexAuthProvider() (LLMProvider, error) {
|
func createCodexAuthProvider(enableWebSearch bool) (LLMProvider, error) {
|
||||||
cred, err := auth.GetCredential("openai")
|
cred, err := auth.GetCredential("openai")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
return nil, fmt.Errorf("loading auth credentials: %w", err)
|
||||||
|
|
@ -216,7 +216,9 @@ func createCodexAuthProvider() (LLMProvider, error) {
|
||||||
if cred == nil {
|
if cred == nil {
|
||||||
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
|
return nil, fmt.Errorf("no credentials for openai. Run: picoclaw auth login --provider openai")
|
||||||
}
|
}
|
||||||
return NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource()), nil
|
p := NewCodexProviderWithTokenSource(cred.AccessToken, cred.AccountID, createCodexTokenSource())
|
||||||
|
p.enableWebSearch = enableWebSearch
|
||||||
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
|
|
@ -241,10 +243,12 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
case "openai", "gpt":
|
case "openai", "gpt":
|
||||||
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
|
if cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != "" {
|
||||||
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
|
if cfg.Providers.OpenAI.AuthMethod == "codex-cli" {
|
||||||
return NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource()), nil
|
c := NewCodexProviderWithTokenSource("", "", CreateCodexCliTokenSource())
|
||||||
|
c.enableWebSearch = cfg.Providers.OpenAI.WebSearch
|
||||||
|
return c, nil
|
||||||
}
|
}
|
||||||
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
||||||
return createCodexAuthProvider()
|
return createCodexAuthProvider(cfg.Providers.OpenAI.WebSearch)
|
||||||
}
|
}
|
||||||
apiKey = cfg.Providers.OpenAI.APIKey
|
apiKey = cfg.Providers.OpenAI.APIKey
|
||||||
apiBase = cfg.Providers.OpenAI.APIBase
|
apiBase = cfg.Providers.OpenAI.APIBase
|
||||||
|
|
@ -369,7 +373,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
|
||||||
|
|
||||||
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
|
case (strings.Contains(lowerModel, "gpt") || strings.HasPrefix(model, "openai/")) && (cfg.Providers.OpenAI.APIKey != "" || cfg.Providers.OpenAI.AuthMethod != ""):
|
||||||
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
if cfg.Providers.OpenAI.AuthMethod == "oauth" || cfg.Providers.OpenAI.AuthMethod == "token" {
|
||||||
return createCodexAuthProvider()
|
return createCodexAuthProvider(cfg.Providers.OpenAI.WebSearch)
|
||||||
}
|
}
|
||||||
apiKey = cfg.Providers.OpenAI.APIKey
|
apiKey = cfg.Providers.OpenAI.APIKey
|
||||||
apiBase = cfg.Providers.OpenAI.APIBase
|
apiBase = cfg.Providers.OpenAI.APIBase
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/bus"
|
"github.com/sipeed/picoclaw/pkg/bus"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/cron"
|
"github.com/sipeed/picoclaw/pkg/cron"
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
@ -29,9 +30,9 @@ type CronTool struct {
|
||||||
|
|
||||||
// NewCronTool creates a new CronTool
|
// NewCronTool creates a new CronTool
|
||||||
// execTimeout: 0 means no timeout, >0 sets the timeout duration
|
// execTimeout: 0 means no timeout, >0 sets the timeout duration
|
||||||
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration) *CronTool {
|
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
|
||||||
execTool := NewExecTool(workspace, restrict)
|
execTool := NewExecToolWithConfig(workspace, restrict, config)
|
||||||
execTool.SetTimeout(execTimeout) // 0 means no timeout
|
execTool.SetTimeout(execTimeout)
|
||||||
return &CronTool{
|
return &CronTool{
|
||||||
cronService: cronService,
|
cronService: cronService,
|
||||||
executor: executor,
|
executor: executor,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -21,8 +22,7 @@ type ExecTool struct {
|
||||||
restrictToWorkspace bool
|
restrictToWorkspace bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
var defaultDenyPatterns = []*regexp.Regexp{
|
||||||
denyPatterns := []*regexp.Regexp{
|
|
||||||
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
|
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`),
|
||||||
regexp.MustCompile(`\bdel\s+/[fq]\b`),
|
regexp.MustCompile(`\bdel\s+/[fq]\b`),
|
||||||
regexp.MustCompile(`\brmdir\s+/s\b`),
|
regexp.MustCompile(`\brmdir\s+/s\b`),
|
||||||
|
|
@ -31,6 +31,73 @@ func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
||||||
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
|
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null)
|
||||||
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
|
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`),
|
||||||
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
|
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`),
|
||||||
|
regexp.MustCompile(`\$\([^)]+\)`),
|
||||||
|
regexp.MustCompile(`\$\{[^}]+\}`),
|
||||||
|
regexp.MustCompile("`[^`]+`"),
|
||||||
|
regexp.MustCompile(`\|\s*sh\b`),
|
||||||
|
regexp.MustCompile(`\|\s*bash\b`),
|
||||||
|
regexp.MustCompile(`;\s*rm\s+-[rf]`),
|
||||||
|
regexp.MustCompile(`&&\s*rm\s+-[rf]`),
|
||||||
|
regexp.MustCompile(`\|\|\s*rm\s+-[rf]`),
|
||||||
|
regexp.MustCompile(`>\s*/dev/null\s*>&?\s*\d?`),
|
||||||
|
regexp.MustCompile(`<<\s*EOF`),
|
||||||
|
regexp.MustCompile(`\$\(\s*cat\s+`),
|
||||||
|
regexp.MustCompile(`\$\(\s*curl\s+`),
|
||||||
|
regexp.MustCompile(`\$\(\s*wget\s+`),
|
||||||
|
regexp.MustCompile(`\$\(\s*which\s+`),
|
||||||
|
regexp.MustCompile(`\bsudo\b`),
|
||||||
|
regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\b`),
|
||||||
|
regexp.MustCompile(`\bchown\b`),
|
||||||
|
regexp.MustCompile(`\bpkill\b`),
|
||||||
|
regexp.MustCompile(`\bkillall\b`),
|
||||||
|
regexp.MustCompile(`\bkill\s+-[9]\b`),
|
||||||
|
regexp.MustCompile(`\bcurl\b.*\|\s*(sh|bash)`),
|
||||||
|
regexp.MustCompile(`\bwget\b.*\|\s*(sh|bash)`),
|
||||||
|
regexp.MustCompile(`\bnpm\s+install\s+-g\b`),
|
||||||
|
regexp.MustCompile(`\bpip\s+install\s+--user\b`),
|
||||||
|
regexp.MustCompile(`\bapt\s+(install|remove|purge)\b`),
|
||||||
|
regexp.MustCompile(`\byum\s+(install|remove)\b`),
|
||||||
|
regexp.MustCompile(`\bdnf\s+(install|remove)\b`),
|
||||||
|
regexp.MustCompile(`\bdocker\s+run\b`),
|
||||||
|
regexp.MustCompile(`\bdocker\s+exec\b`),
|
||||||
|
regexp.MustCompile(`\bgit\s+push\b`),
|
||||||
|
regexp.MustCompile(`\bgit\s+force\b`),
|
||||||
|
regexp.MustCompile(`\bssh\b.*@`),
|
||||||
|
regexp.MustCompile(`\beval\b`),
|
||||||
|
regexp.MustCompile(`\bsource\s+.*\.sh\b`),
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExecTool(workingDir string, restrict bool) *ExecTool {
|
||||||
|
return NewExecToolWithConfig(workingDir, restrict, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewExecToolWithConfig(workingDir string, restrict bool, config *config.Config) *ExecTool {
|
||||||
|
denyPatterns := make([]*regexp.Regexp, 0)
|
||||||
|
|
||||||
|
enableDenyPatterns := true
|
||||||
|
if config != nil {
|
||||||
|
execConfig := config.Tools.Exec
|
||||||
|
enableDenyPatterns = execConfig.EnableDenyPatterns
|
||||||
|
if enableDenyPatterns {
|
||||||
|
if len(execConfig.CustomDenyPatterns) > 0 {
|
||||||
|
fmt.Printf("Using custom deny patterns: %v\n", execConfig.CustomDenyPatterns)
|
||||||
|
for _, pattern := range execConfig.CustomDenyPatterns {
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Invalid custom deny pattern %q: %v\n", pattern, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
denyPatterns = append(denyPatterns, re)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
denyPatterns = append(denyPatterns, defaultDenyPatterns...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If deny patterns are disabled, we won't add any patterns, allowing all commands.
|
||||||
|
fmt.Println("Warning: deny patterns are disabled. All commands will be allowed.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
denyPatterns = append(denyPatterns, defaultDenyPatterns...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ExecTool{
|
return &ExecTool{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue