refactor: adopt upstream versions for docs, config, and low-diff Go files

Replace 30+ files with upstream's exact versions to eliminate merge
conflicts. Fork-only additions (interfaces, struct fields, methods)
are re-added at non-conflicting positions or in separate _ext.go files.

Files replaced: READMEs, CI configs, docs, Makefile, and ~20 Go source
files with <30 line diffs from upstream.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-03-13 09:46:34 +09:00
parent 8eaf46e87e
commit 9caa237639
47 changed files with 760 additions and 346 deletions

View file

@ -1,7 +1,7 @@
name: PR name: PR
on: on:
pull_request: {} pull_request: { }
jobs: jobs:
lint: lint:
@ -16,9 +16,6 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Run go generate - name: Run go generate
run: go generate ./... run: go generate ./...
@ -58,26 +55,8 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Run go generate - name: Run go generate
run: go generate ./... run: go generate ./...
- name: Run frontend tests
run: |
pnpm --dir pkg/miniapp/frontend install --frozen-lockfile
pnpm --dir pkg/miniapp/frontend test
- name: Run go test - name: Run go test
run: go test ./... run: go test ./...

View file

@ -65,6 +65,14 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
run: corepack enable && corepack prepare pnpm@latest --activate
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
@ -96,6 +104,11 @@ jobs:
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 }} GOVERSION: ${{ steps.setup-go.outputs.go-version }}
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
- name: Apply release flags - name: Apply release flags
shell: bash shell: bash

12
.gitignore vendored
View file

@ -47,14 +47,12 @@ docs/plans/
# Added by goreleaser init: # Added by goreleaser init:
dist/ dist/
!pkg/miniapp/static/dist/ *.vite/
!pkg/miniapp/static/dist/**
# Windows Application Icon/Resource # Windows Application Icon/Resource
*.syso *.syso
# Keep embedded backend dist directory placeholder in VCS
# Frontend dependencies !web/backend/dist/
node_modules/ web/backend/dist/*
!web/backend/dist/.gitkeep

View file

@ -6,8 +6,9 @@ before:
hooks: hooks:
- go mod tidy - go mod tidy
- go generate ./... - go generate ./...
- sh -c 'cd web/frontend && pnpm install && pnpm build:backend'
- go install github.com/tc-hib/go-winres@latest - go install github.com/tc-hib/go-winres@latest
- go-winres make --in cmd/picoclaw-launcher/winres/winres.json --out cmd/picoclaw-launcher/rsrc --product-version={{ .Version }} --file-version={{ .Version }} - go-winres make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }}
builds: builds:
- id: picoclaw - id: picoclaw
@ -17,10 +18,10 @@ builds:
- stdjson - stdjson
ldflags: ldflags:
- -s -w - -s -w
- -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.version={{ .Version }} - -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.gitCommit={{ .ShortCommit }} - -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.buildTime={{ .Date }} - -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/cmd/picoclaw/internal.goVersion={{ .Env.GOVERSION }} - -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ .Env.GOVERSION }}
goos: goos:
- linux - linux
- windows - windows
@ -32,9 +33,13 @@ builds:
- riscv64 - riscv64
- loong64 - loong64
- arm - arm
- s390x
- mipsle
goarm: goarm:
- "6" - "6"
- "7" - "7"
gomips:
- softfloat
main: ./cmd/picoclaw main: ./cmd/picoclaw
ignore: ignore:
- goos: windows - goos: windows
@ -59,10 +64,14 @@ builds:
- riscv64 - riscv64
- loong64 - loong64
- arm - arm
- s390x
- mipsle
goarm: goarm:
- "6" - "6"
- "7" - "7"
main: ./cmd/picoclaw-launcher gomips:
- softfloat
main: ./web/backend
ignore: ignore:
- goos: windows - goos: windows
goarch: arm goarch: arm
@ -86,9 +95,13 @@ builds:
- riscv64 - riscv64
- loong64 - loong64
- arm - arm
- s390x
- mipsle
goarm: goarm:
- "6" - "6"
- "7" - "7"
gomips:
- softfloat
main: ./cmd/picoclaw-launcher-tui main: ./cmd/picoclaw-launcher-tui
ignore: ignore:
- goos: windows - goos: windows
@ -103,15 +116,49 @@ dockers_v2:
- picoclaw - picoclaw
images: images:
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw" - "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
- "docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}" - 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}'
tags: tags:
- "{{ .Tag }}" - '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}{{ .Tag }}{{ end }}'
- "latest" - '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}latest{{ end }}'
platforms: platforms:
- linux/amd64 - linux/amd64
- linux/arm64 - linux/arm64
- linux/riscv64 - linux/riscv64
- id: picoclaw-launcher
dockerfile: docker/Dockerfile.goreleaser.launcher
ids:
- picoclaw
- picoclaw-launcher
- picoclaw-launcher-tui
images:
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
- 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}'
tags:
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}{{ .Tag }}-launcher{{ end }}'
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}launcher{{ end }}'
platforms:
- linux/amd64
- linux/arm64
- linux/riscv64
notarize:
macos:
- enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
ids:
- picoclaw
- picoclaw-launcher
- picoclaw-launcher-tui
sign:
certificate: "{{.Env.MACOS_SIGN_P12}}"
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
notarize:
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
key: "{{.Env.MACOS_NOTARY_KEY}}"
wait: true
timeout: 20m
archives: archives:
- formats: [tar.gz] - formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`. # this name template makes the OS and Arch compatible with the results of `uname`.
@ -129,7 +176,7 @@ archives:
nfpms: nfpms:
- id: picoclaw - id: picoclaw
builds: ids:
- picoclaw - picoclaw
- picoclaw-launcher - picoclaw-launcher
- picoclaw-launcher-tui - picoclaw-launcher-tui
@ -149,6 +196,11 @@ nfpms:
- rpm - rpm
- deb - deb
bindir: /usr/bin bindir: /usr/bin
contents:
- src: web/picoclaw-launcher.desktop
dst: /usr/share/applications/picoclaw-launcher.desktop
- src: web/picoclaw-launcher.png
dst: /usr/share/icons/hicolor/512x512/apps/picoclaw-launcher.png
changelog: changelog:
sort: asc sort: asc
@ -163,6 +215,7 @@ changelog:
# lzma: true # lzma: true
release: release:
disable: '{{ isEnvSet "NIGHTLY_BUILD" }}'
footer: >- footer: >-
--- ---

View file

@ -11,8 +11,8 @@ 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}')
INTERNAL=github.com/sipeed/picoclaw/cmd/picoclaw/internal CONFIG_PKG=github.com/sipeed/picoclaw/pkg/config
LDFLAGS=-ldflags "-X $(INTERNAL).version=$(VERSION) -X $(INTERNAL).gitCommit=$(GIT_COMMIT) -X $(INTERNAL).buildTime=$(BUILD_TIME) -X $(INTERNAL).goVersion=$(GO_VERSION) -s -w" LDFLAGS=-ldflags "-X $(CONFIG_PKG).Version=$(VERSION) -X $(CONFIG_PKG).GitCommit=$(GIT_COMMIT) -X $(CONFIG_PKG).BuildTime=$(BUILD_TIME) -X $(CONFIG_PKG).GoVersion=$(GO_VERSION) -s -w"
# Go variables # Go variables
GO?=CGO_ENABLED=0 go GO?=CGO_ENABLED=0 go
@ -110,12 +110,18 @@ build: generate
@$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BINARY_PATH) ./$(CMD_DIR) @$(GO) build $(GOFLAGS) $(LDFLAGS) -o $(BINARY_PATH) ./$(CMD_DIR)
@echo "Build complete: $(BINARY_PATH)" @echo "Build complete: $(BINARY_PATH)"
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME) @ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
@if [ "$(PLATFORM)" = "linux" ] || [ "$(PLATFORM)" = "darwin" ]; then \
echo "Install to /usr/local/bin:"; \ ## build-launcher: Build the picoclaw-launcher (web console) binary
echo " sudo install -m 755 $(BINARY_PATH) /usr/local/bin/$(BINARY_NAME)"; \ build-launcher:
else \ @echo "Building picoclaw-launcher for $(PLATFORM)/$(ARCH)..."
echo "Install hint skipped: /usr/local/bin command is for Linux/macOS (current: $(PLATFORM))."; \ @mkdir -p $(BUILD_DIR)
@if [ ! -f web/backend/dist/index.html ]; then \
echo "Building frontend..."; \
cd web/frontend && pnpm install && pnpm build:backend; \
fi fi
@$(GO) build $(GOFLAGS) -o $(BUILD_DIR)/picoclaw-launcher-$(PLATFORM)-$(ARCH) ./web/backend
@ln -sf picoclaw-launcher-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/picoclaw-launcher
@echo "Build complete: $(BUILD_DIR)/picoclaw-launcher"
## build-whatsapp-native: Build with WhatsApp native (whatsmeow) support; larger binary ## build-whatsapp-native: Build with WhatsApp native (whatsmeow) support; larger binary
build-whatsapp-native: generate build-whatsapp-native: generate

View file

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512"> <img src="assets/logo.webp" alt="PicoClaw" width="512">
<h1>PicoClaw : Assistant IA Ultra-Efficace en Go</h1> <h1>PicoClaw : Assistant IA Ultra-Efficace en Go</h1>
@ -206,9 +206,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
### 🚀 Démarrage Rapide ### 🚀 Démarrage Rapide
> [!TIP] > [!TIP]
> Configurez votre clé API dans `~/.picoclaw/config.json`. > Configurez votre clé API dans `~/.picoclaw/config.json`. Obtenez des clés API : [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). La recherche web est optionnelle — obtenez gratuitement l'[API Tavily](https://tavily.com) (1000 requêtes gratuites/mois) ou l'[API Brave Search](https://brave.com/search/api) (2000 requêtes gratuites/mois).
> Obtenir des clés API : [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> La recherche web est **optionnelle** — obtenez gratuitement l'[API Brave Search](https://brave.com/search/api) (2000 requêtes gratuites/mois) ou utilisez le repli automatique intégré.
**1. Initialiser** **1. Initialiser**
@ -222,8 +220,13 @@ picoclaw onboard
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key", "api_key": "sk-your-openai-key",
"request_timeout": 300, "request_timeout": 300,
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
@ -231,7 +234,7 @@ picoclaw onboard
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model_name": "gpt4" "model_name": "gpt-5.4"
} }
}, },
"channels": { "channels": {
@ -649,7 +652,6 @@ PicoClaw stocke les données dans votre workspace configuré (par défaut : `~/.
├── HEARTBEAT.md # Invites de tâches périodiques (vérifiées toutes les 30 min) ├── HEARTBEAT.md # Invites de tâches périodiques (vérifiées toutes les 30 min)
├── IDENTITY.md # Identité de l'Agent ├── IDENTITY.md # Identité de l'Agent
├── SOUL.md # Âme de l'Agent ├── SOUL.md # Âme de l'Agent
├── TOOLS.md # Description des outils
└── USER.md # Préférences utilisateur └── USER.md # Préférences utilisateur
``` ```
@ -978,8 +980,10 @@ Cette conception permet également le **support multi-agent** avec une sélectio
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obtenir Clé](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obtenir Clé](https://openrouter.ai/keys) |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obtenir Clé](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obtenir Clé](https://cerebras.ai) |
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obtenir Clé](https://console.volcengine.com) | | **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obtenir Clé](https://console.volcengine.com) |
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | | **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Obtenir Clé](https://console.volcengine.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Obtenir une clé](https://longcat.chat/platform) |
| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth uniquement | | **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth uniquement |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
@ -989,8 +993,13 @@ Cette conception permet également le **support multi-agent** avec une sélectio
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key" "api_key": "sk-your-openai-key"
}, },
{ {
@ -1006,7 +1015,7 @@ Cette conception permet également le **support multi-agent** avec une sélectio
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model": "gpt-5.2" "model": "gpt-5.4"
} }
} }
} }
@ -1017,8 +1026,17 @@ Cette conception permet également le **support multi-agent** avec une sélectio
**OpenAI** **OpenAI**
```json ```json
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-..."
}
```
**VolcEngine (Doubao)**
```json
{
"model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest",
"api_key": "sk-..." "api_key": "sk-..."
} }
``` ```
@ -1061,14 +1079,14 @@ Configurez plusieurs points de terminaison pour le même nom de modèle—PicoCl
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_key": "sk-key1" "api_key": "sk-key1"
}, },
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_key": "sk-key2" "api_key": "sk-key2"
} }
@ -1200,6 +1218,13 @@ Cela se produit lorsqu'une autre instance du bot est en cours d'exécution. Assu
| Service | Offre Gratuite | Cas d'Utilisation | | Service | Offre Gratuite | Cas d'Utilisation |
| ---------------- | -------------------- | ------------------------------------- | | ---------------- | -------------------- | ------------------------------------- |
| **OpenRouter** | 200K tokens/mois | Multiples modèles (Claude, GPT-4, etc.) | | **OpenRouter** | 200K tokens/mois | Multiples modèles (Claude, GPT-4, etc.) |
| **Zhipu** | 200K tokens/mois | Idéal pour les utilisateurs chinois | | **Volcengine CodingPlan** | 9,9¥/premier mois | Idéal pour les utilisateurs chinois, multiples modèles SOTA (Doubao, DeepSeek, etc.) |
| **Zhipu** | 200K tokens/mois | Convient aux utilisateurs chinois |
| **Brave Search** | 2000 requêtes/mois | Fonctionnalité de recherche web | | **Brave Search** | 2000 requêtes/mois | Fonctionnalité de recherche web |
| **Groq** | Offre gratuite dispo | Inférence ultra-rapide (Llama, Mixtral) | | **Groq** | Offre gratuite dispo | Inférence ultra-rapide (Llama, Mixtral) |
---
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
</div>

View file

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512"> <img src="assets/logo.webp" alt="PicoClaw" width="512">
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1> <h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
@ -168,9 +168,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
### 🚀 クイックスタート(ネイティブ) ### 🚀 クイックスタート(ネイティブ)
> [!TIP] > [!TIP]
> `~/.picoclaw/config.json` に API キーを設定してください。 > `~/.picoclaw/config.json` に API キーを設定してください。API キーの取得先: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)。Web 検索は **任意** です — 無料の [Tavily API](https://tavily.com) (月 1000 クエリ無料) または [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料)。
> API キーの取得先: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> Web 検索は **任意** です - 無料の [Tavily API](https://tavily.com) (月 1000 クエリ無料) または [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料)
**1. 初期化** **1. 初期化**
@ -184,8 +182,13 @@ picoclaw onboard
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key", "api_key": "sk-your-openai-key",
"request_timeout": 300, "request_timeout": 300,
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
@ -193,7 +196,7 @@ picoclaw onboard
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model_name": "gpt4" "model_name": "gpt-5.4"
} }
}, },
"channels": { "channels": {
@ -610,7 +613,6 @@ PicoClaw は設定されたワークスペース(デフォルト: `~/.picoclaw
├── HEARTBEAT.md # 定期タスクプロンプト30分ごとに確認 ├── HEARTBEAT.md # 定期タスクプロンプト30分ごとに確認
├── IDENTITY.md # エージェントのアイデンティティ ├── IDENTITY.md # エージェントのアイデンティティ
├── SOUL.md # エージェントのソウル ├── SOUL.md # エージェントのソウル
├── TOOLS.md # ツールの説明
└── USER.md # ユーザー設定 └── USER.md # ユーザー設定
``` ```
@ -919,8 +921,10 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [キーを取得](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [キーを取得](https://openrouter.ai/keys) |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | ローカル | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | ローカル |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [キーを取得](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [キーを取得](https://cerebras.ai) |
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [キーを取得](https://console.volcengine.com) | | **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [キーを取得](https://console.volcengine.com) |
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | | **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [キーを取得](https://console.volcengine.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [キーを取得](https://longcat.chat/platform) |
| **Antigravity** | `antigravity/` | Google Cloud | カスタム | OAuthのみ | | **Antigravity** | `antigravity/` | Google Cloud | カスタム | OAuthのみ |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
@ -930,8 +934,13 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key" "api_key": "sk-your-openai-key"
}, },
{ {
@ -947,7 +956,7 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model": "gpt-5.2" "model": "gpt-5.4"
} }
} }
} }
@ -958,8 +967,17 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
**OpenAI** **OpenAI**
```json ```json
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-..."
}
```
**VolcEngine (Doubao)**
```json
{
"model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest",
"api_key": "sk-..." "api_key": "sk-..."
} }
``` ```
@ -1002,14 +1020,14 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_key": "sk-key1" "api_key": "sk-key1"
}, },
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_key": "sk-key2" "api_key": "sk-key2"
} }
@ -1120,9 +1138,16 @@ Web 検索を有効にするには:
| サービス | 無料枠 | ユースケース | | サービス | 無料枠 | ユースケース |
|---------|--------|------------| |---------|--------|------------|
| **OpenRouter** | 月 200K トークン | 複数モデルClaude, GPT-4 など) | | **OpenRouter** | 月 200K トークン | 複数モデルClaude, GPT-4 など) |
| **Zhipu** | 月 200K トークン | 中国ユーザー向け最適 | | **Volcengine CodingPlan** | 9.9元/初月 | 中国ユーザーに最適、複数のSOTAモデルDoubao、DeepSeek等 |
| **Zhipu** | 月 200K トークン | 中国ユーザーに適している |
| **Qwen** | 無料枠あり | 通義千問 (Qwen) | | **Qwen** | 無料枠あり | 通義千問 (Qwen) |
| **Brave Search** | 月 2000 クエリ | Web 検索機能 | | **Brave Search** | 月 2000 クエリ | Web 検索機能 |
| **Tavily** | 月 1000 クエリ | AI エージェント検索最適化 | | **Tavily** | 月 1000 クエリ | AI エージェント検索最適化 |
| **Groq** | 無料枠あり | 高速推論Llama, Mixtral | | **Groq** | 無料枠あり | 高速推論Llama, Mixtral |
| **Cerebras** | 無料枠あり | 高速推論Llama, Qwen など) | | **Cerebras** | 無料枠あり | 高速推論Llama, Qwen など) |
---
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
</div>

View file

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512"> <img src="assets/logo.webp" alt="PicoClaw" width="512">
<h1>PicoClaw: Ultra-Efficient AI Assistant in Go</h1> <h1>PicoClaw: Ultra-Efficient AI Assistant in Go</h1>
@ -194,6 +194,19 @@ docker compose -f docker/docker-compose.yml logs -f picoclaw-gateway
docker compose -f docker/docker-compose.yml --profile gateway down docker compose -f docker/docker-compose.yml --profile gateway down
``` ```
### Launcher Mode (Web Console)
The `launcher` image includes all three binaries (`picoclaw`, `picoclaw-launcher`, `picoclaw-launcher-tui`) and starts the web console by default, which provides a browser-based UI for configuration and chat.
```bash
docker compose -f docker/docker-compose.yml --profile launcher up -d
```
Open http://localhost:18800 in your browser. The launcher manages the gateway process automatically.
> [!WARNING]
> The web console does not yet support authentication. Avoid exposing it to the public internet.
### Agent Mode (One-shot) ### Agent Mode (One-shot)
```bash ```bash
@ -214,9 +227,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
### 🚀 Quick Start ### 🚀 Quick Start
> [!TIP] > [!TIP]
> Set your API key in `~/.picoclaw/config.json`. > Set your API Key in `~/.picoclaw/config.json`. Get API Keys: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Web search is optional — get a free [Tavily API](https://tavily.com) (1000 free queries/month) or [Brave Search API](https://brave.com/search/api) (2000 free queries/month).
> Get API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> Web Search is **optional** - get free [Tavily API](https://tavily.com) (1000 free queries/month), [SearXNG](https://github.com/searxng/searxng) (free, self-hosted) or [Brave Search API](https://brave.com/search/api) (2000 free queries/month) or use built-in auto fallback.
**1. Initialize** **1. Initialize**
@ -231,7 +242,7 @@ picoclaw onboard
"agents": { "agents": {
"defaults": { "defaults": {
"workspace": "~/.picoclaw/workspace", "workspace": "~/.picoclaw/workspace",
"model_name": "gpt4", "model_name": "gpt-5.4",
"max_tokens": 8192, "max_tokens": 8192,
"temperature": 0.7, "temperature": 0.7,
"max_tool_iterations": 20 "max_tool_iterations": 20
@ -239,8 +250,13 @@ picoclaw onboard
}, },
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "your-api-key", "api_key": "your-api-key",
"request_timeout": 300 "request_timeout": 300
}, },
@ -774,7 +790,6 @@ PicoClaw stores data in your configured workspace (default: `~/.picoclaw/workspa
├── HEARTBEAT.md # Periodic task prompts (checked every 30 min) ├── HEARTBEAT.md # Periodic task prompts (checked every 30 min)
├── IDENTITY.md # Agent identity ├── IDENTITY.md # Agent identity
├── SOUL.md # Agent soul ├── SOUL.md # Agent soul
├── TOOLS.md # Tool descriptions
└── USER.md # User preferences └── USER.md # User preferences
``` ```
@ -1018,9 +1033,11 @@ This design also enables **multi-agent support** with flexible provider selectio
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key | | **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
| **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) | | **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) |
| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | | **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) |
| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) | | **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Get Key](https://longcat.chat/platform) |
| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth only | | **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth only |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
@ -1030,8 +1047,13 @@ This design also enables **multi-agent support** with flexible provider selectio
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key" "api_key": "sk-your-openai-key"
}, },
{ {
@ -1047,7 +1069,7 @@ This design also enables **multi-agent support** with flexible provider selectio
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model": "gpt-5.2" "model": "gpt-5.4"
} }
} }
} }
@ -1059,8 +1081,18 @@ This design also enables **multi-agent support** with flexible provider selectio
```json ```json
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-..."
}
```
**VolcEngine (Doubao)**
```json
{
"model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest",
"api_key": "sk-..." "api_key": "sk-..."
} }
``` ```
@ -1139,14 +1171,14 @@ Configure multiple endpoints for the same model name—PicoClaw will automatical
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_key": "sk-key1" "api_key": "sk-key1"
}, },
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_key": "sk-key2" "api_key": "sk-key2"
} }
@ -1486,8 +1518,16 @@ This happens when another instance of the bot is running. Make sure only one `pi
| Service | Free Tier | Use Case | | Service | Free Tier | Use Case |
| ---------------- | ------------------------ | ------------------------------------- | | ---------------- | ------------------------ | ------------------------------------- |
| **OpenRouter** | 200K tokens/month | Multiple models (Claude, GPT-4, etc.) | | **OpenRouter** | 200K tokens/month | Multiple models (Claude, GPT-4, etc.) |
| **Zhipu** | 200K tokens/month | Best for Chinese users | | **Volcengine CodingPlan** | ¥9.9/first month | Best for Chinese users, multiple SOTA models (Doubao, DeepSeek, etc.) |
| **Zhipu** | 200K tokens/month | Suitable for Chinese users |
| **Brave Search** | Paid ($5/1000 queries) | Web search functionality | | **Brave Search** | Paid ($5/1000 queries) | Web search functionality |
| **SearXNG** | Unlimited (self-hosted) | Privacy-focused metasearch (70+ engines) | | **SearXNG** | Unlimited (self-hosted) | Privacy-focused metasearch (70+ engines) |
| **Groq** | Free tier available | Fast inference (Llama, Mixtral) | | **Groq** | Free tier available | Fast inference (Llama, Mixtral) |
| **Cerebras** | Free tier available | Fast inference (Llama, Qwen, etc.) | | **Cerebras** | Free tier available | Fast inference (Llama, Qwen, etc.) |
| **LongCat** | Up to 5M tokens/day | Fast inference (free tier) |
---
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
</div>

View file

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512"> <img src="assets/logo.webp" alt="PicoClaw" width="512">
<h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1> <h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1>
@ -207,9 +207,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
### 🚀 Início Rápido ### 🚀 Início Rápido
> [!TIP] > [!TIP]
> Configure sua API key em `~/.picoclaw/config.json`. > Configure sua API key em `~/.picoclaw/config.json`. Obtenha API keys: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Busca web é **opcional** — obtenha a [API Tavily](https://tavily.com) gratuita (1000 consultas grátis/mês) ou a [Brave Search API](https://brave.com/search/api) (2000 consultas grátis/mês).
> 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 grátis/mês) ou use o fallback automático integrado.
**1. Inicializar** **1. Inicializar**
@ -223,8 +221,13 @@ picoclaw onboard
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key", "api_key": "sk-your-openai-key",
"request_timeout": 300, "request_timeout": 300,
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
@ -232,7 +235,7 @@ picoclaw onboard
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model_name": "gpt4" "model_name": "gpt-5.4"
} }
}, },
"tools": { "tools": {
@ -645,7 +648,6 @@ O PicoClaw armazena dados no workspace configurado (padrão: `~/.picoclaw/worksp
├── HEARTBEAT.md # Prompts de tarefas periodicas (verificado a cada 30 min) ├── HEARTBEAT.md # Prompts de tarefas periodicas (verificado a cada 30 min)
├── IDENTITY.md # Identidade do Agente ├── IDENTITY.md # Identidade do Agente
├── SOUL.md # Alma do Agente ├── SOUL.md # Alma do Agente
├── TOOLS.md # Descrição das ferramentas
└── USER.md # Preferencias do usuario └── USER.md # Preferencias do usuario
``` ```
@ -974,8 +976,10 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obter Chave](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obter Chave](https://openrouter.ai/keys) |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obter Chave](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obter Chave](https://cerebras.ai) |
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obter Chave](https://console.volcengine.com) | | **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obter Chave](https://console.volcengine.com) |
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | | **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Obter Chave](https://console.volcengine.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Obter Chave](https://longcat.chat/platform) |
| **Antigravity** | `antigravity/` | Google Cloud | Custom | Apenas OAuth | | **Antigravity** | `antigravity/` | Google Cloud | Custom | Apenas OAuth |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
@ -985,8 +989,13 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key" "api_key": "sk-your-openai-key"
}, },
{ {
@ -1002,7 +1011,7 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model": "gpt-5.2" "model": "gpt-5.4"
} }
} }
} }
@ -1013,8 +1022,17 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
**OpenAI** **OpenAI**
```json ```json
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-..."
}
```
**VolcEngine (Doubao)**
```json
{
"model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest",
"api_key": "sk-..." "api_key": "sk-..."
} }
``` ```
@ -1057,14 +1075,14 @@ Configure vários endpoints para o mesmo nome de modelo—PicoClaw fará round-r
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_key": "sk-key1" "api_key": "sk-key1"
}, },
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_key": "sk-key2" "api_key": "sk-key2"
} }
@ -1196,7 +1214,14 @@ Isso acontece quando outra instância do bot está em execução. Certifique-se
| Serviço | Plano Gratuito | Caso de Uso | | Serviço | Plano Gratuito | Caso de Uso |
| --- | --- | --- | | --- | --- | --- |
| **OpenRouter** | 200K tokens/mês | Múltiplos modelos (Claude, GPT-4, etc.) | | **OpenRouter** | 200K tokens/mês | Múltiplos modelos (Claude, GPT-4, etc.) |
| **Zhipu** | 200K tokens/mês | Melhor para usuários chineses | | **Volcengine CodingPlan** | ¥9,9/primeiro mês | Ideal para usuários chineses, múltiplos modelos SOTA (Doubao, DeepSeek, etc.) |
| **Zhipu** | 200K tokens/mês | Adequado para usuários chineses |
| **Brave Search** | 2000 consultas/mês | Funcionalidade de busca web | | **Brave Search** | 2000 consultas/mês | Funcionalidade de busca web |
| **Groq** | Plano gratuito disponível | Inferência ultra-rápida (Llama, Mixtral) | | **Groq** | Plano gratuito disponível | Inferência ultra-rápida (Llama, Mixtral) |
| **Cerebras** | Plano gratuito disponível | Inferência ultra-rápida (Llama 3.3 70B) | | **Cerebras** | Plano gratuito disponível | Inferência ultra-rápida (Llama 3.3 70B) |
---
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
</div>

View file

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512"> <img src="assets/logo.webp" alt="PicoClaw" width="512">
<h1>PicoClaw: Trợ lý AI Siêu Nhẹ viết bằng Go</h1> <h1>PicoClaw: Trợ lý AI Siêu Nhẹ viết bằng Go</h1>
@ -187,9 +187,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
### 🚀 Bắt đầu nhanh ### 🚀 Bắt đầu nhanh
> [!TIP] > [!TIP]
> Thiết lập API key trong `~/.picoclaw/config.json`. > Thiết lập API key trong `~/.picoclaw/config.json`. Lấy API key: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Tìm kiếm web là **tùy chọn** — lấy [Tavily API](https://tavily.com) miễn phí (1000 truy vấn/tháng) hoặc [Brave Search API](https://brave.com/search/api) (2000 truy vấn/tháng).
> Lấy API key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> Tìm kiếm web là **tùy chọn** — lấy [Brave Search API](https://brave.com/search/api) miễn phí (2000 truy vấn/tháng) hoặc dùng tính năng auto fallback tích hợp sẵn.
**1. Khởi tạo** **1. Khởi tạo**
@ -203,8 +201,13 @@ picoclaw onboard
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key", "api_key": "sk-your-openai-key",
"request_timeout": 300, "request_timeout": 300,
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
@ -617,7 +620,6 @@ PicoClaw lưu trữ dữ liệu trong workspace đã cấu hình (mặc định:
├── HEARTBEAT.md # Prompt tác vụ định kỳ (kiểm tra mỗi 30 phút) ├── HEARTBEAT.md # Prompt tác vụ định kỳ (kiểm tra mỗi 30 phút)
├── IDENTITY.md # Danh tính Agent ├── IDENTITY.md # Danh tính Agent
├── SOUL.md # Tâm hồn/Tính cách Agent ├── SOUL.md # Tâm hồn/Tính cách Agent
├── TOOLS.md # Mô tả công cụ
└── USER.md # Tùy chọn người dùng └── USER.md # Tùy chọn người dùng
``` ```
@ -943,8 +945,10 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Lấy Khóa](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Lấy Khóa](https://openrouter.ai/keys) |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Lấy Khóa](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Lấy Khóa](https://cerebras.ai) |
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Lấy Khóa](https://console.volcengine.com) | | **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Lấy Khóa](https://console.volcengine.com) |
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | | **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Lấy Khóa](https://console.volcengine.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Lấy Key](https://longcat.chat/platform) |
| **Antigravity** | `antigravity/` | Google Cloud | Tùy chỉnh | Chỉ OAuth | | **Antigravity** | `antigravity/` | Google Cloud | Tùy chỉnh | Chỉ OAuth |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
@ -954,8 +958,13 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key" "api_key": "sk-your-openai-key"
}, },
{ {
@ -971,7 +980,7 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model": "gpt-5.2" "model": "gpt-5.4"
} }
} }
} }
@ -982,8 +991,17 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
**OpenAI** **OpenAI**
```json ```json
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-..."
}
```
**VolcEngine (Doubao)**
```json
{
"model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest",
"api_key": "sk-..." "api_key": "sk-..."
} }
``` ```
@ -1026,14 +1044,14 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_key": "sk-key1" "api_key": "sk-key1"
}, },
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_key": "sk-key2" "api_key": "sk-key2"
} }
@ -1165,6 +1183,13 @@ Một số nhà cung cấp (như Zhipu) có bộ lọc nội dung nghiêm ngặt
| Dịch vụ | Gói miễn phí | Trường hợp sử dụng | | Dịch vụ | Gói miễn phí | Trường hợp sử dụng |
| --- | --- | --- | | --- | --- | --- |
| **OpenRouter** | 200K tokens/tháng | Đa model (Claude, GPT-4, v.v.) | | **OpenRouter** | 200K tokens/tháng | Đa model (Claude, GPT-4, v.v.) |
| **Zhipu** | 200K tokens/tháng | Tốt nhất cho người dùng Trung Quốc | | **Volcengine CodingPlan** | ¥9.9/tháng đầu | Tốt nhất cho người dùng Trung Quốc, nhiều mô hình SOTA (Doubao, DeepSeek, v.v.) |
| **Zhipu** | 200K tokens/tháng | Phù hợp cho người dùng Trung Quốc |
| **Brave Search** | 2000 truy vấn/tháng | Chức năng tìm kiếm web | | **Brave Search** | 2000 truy vấn/tháng | Chức năng tìm kiếm web |
| **Groq** | Có gói miễn phí | Suy luận siêu nhanh (Llama, Mixtral) | | **Groq** | Có gói miễn phí | Suy luận siêu nhanh (Llama, Mixtral) |
---
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
</div>

View file

@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512"> <img src="assets/logo.webp" alt="PicoClaw" width="512">
<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1> <h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>
@ -208,9 +208,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
### 🚀 快速开始 ### 🚀 快速开始
> [!TIP] > [!TIP]
> 在 `~/.picoclaw/config.json` 中设置您的 API Key。 > 在 `~/.picoclaw/config.json` 中设置您的 API Key。获取 API Key: [火山引擎 (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)。网络搜索是 **可选的** — 获取免费的 [Tavily API](https://tavily.com) (每月 1000 次免费查询) 或 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)。
> 获取 API Key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> 网络搜索是 **可选的** - 获取免费的 [Tavily API](https://tavily.com) (每月 1000 次免费查询) 或 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)
**1. 初始化 (Initialize)** **1. 初始化 (Initialize)**
@ -226,7 +224,7 @@ picoclaw onboard
"agents": { "agents": {
"defaults": { "defaults": {
"workspace": "~/.picoclaw/workspace", "workspace": "~/.picoclaw/workspace",
"model_name": "gpt4", "model_name": "gpt-5.4",
"max_tokens": 8192, "max_tokens": 8192,
"temperature": 0.7, "temperature": 0.7,
"max_tool_iterations": 20 "max_tool_iterations": 20
@ -234,8 +232,13 @@ picoclaw onboard
}, },
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "your-api-key", "api_key": "your-api-key",
"request_timeout": 300 "request_timeout": 300
}, },
@ -365,7 +368,6 @@ PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/work
├── HEARTBEAT.md # 周期性任务提示词 (每 30 分钟检查一次) ├── HEARTBEAT.md # 周期性任务提示词 (每 30 分钟检查一次)
├── IDENTITY.md # Agent 身份设定 ├── IDENTITY.md # Agent 身份设定
├── SOUL.md # Agent 灵魂/性格 ├── SOUL.md # Agent 灵魂/性格
├── TOOLS.md # 工具描述
└── USER.md # 用户偏好 └── USER.md # 用户偏好
``` ```
@ -515,8 +517,10 @@ Agent 读取 HEARTBEAT.md
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) | | **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 | | **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 |
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) | | **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) |
| **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) | | **火山引擎Doubao** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) |
| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - | | **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) |
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [获取密钥](https://longcat.chat/platform) |
| **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth | | **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth |
| **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - | | **GitHub Copilot** | `github-copilot/` | `localhost:4321` | gRPC | - |
@ -526,8 +530,13 @@ Agent 读取 HEARTBEAT.md
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "ark-code-latest",
"model": "openai/gpt-5.2", "model": "volcengine/ark-code-latest",
"api_key": "sk-your-api-key"
},
{
"model_name": "gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key" "api_key": "sk-your-openai-key"
}, },
{ {
@ -543,7 +552,7 @@ Agent 读取 HEARTBEAT.md
], ],
"agents": { "agents": {
"defaults": { "defaults": {
"model": "gpt-5.2" "model": "gpt-5.4"
} }
} }
} }
@ -555,8 +564,18 @@ Agent 读取 HEARTBEAT.md
```json ```json
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-..."
}
```
**火山引擎Doubao**
```json
{
"model_name": "ark-code-latest",
"model": "volcengine/ark-code-latest",
"api_key": "sk-..." "api_key": "sk-..."
} }
``` ```
@ -622,14 +641,14 @@ Agent 读取 HEARTBEAT.md
{ {
"model_list": [ "model_list": [
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api1.example.com/v1", "api_base": "https://api1.example.com/v1",
"api_key": "sk-key1" "api_key": "sk-key1"
}, },
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_base": "https://api2.example.com/v1", "api_base": "https://api2.example.com/v1",
"api_key": "sk-key2" "api_key": "sk-key2"
} }
@ -875,7 +894,15 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
| 服务 | 免费层级 | 适用场景 | | 服务 | 免费层级 | 适用场景 |
| --- | --- | --- | | --- | --- | --- |
| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) | | **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 | | **火山引擎 CodingPlan** | 9.9 元/首月 | 最适合国内用户,多种 SOTA 模型豆包、DeepSeek 等) |
| **智谱 (Zhipu)** | 200K tokens/月 | 适合中国用户 |
| **Brave Search** | 2000 次查询/月 | 网络搜索功能 | | **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
| **Tavily** | 1000 次查询/月 | AI Agent 搜索优化 | | **Tavily** | 1000 次查询/月 | AI Agent 搜索优化 |
| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) | | **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
| **LongCat** | 最多 5M tokens/天 | 推理速度快 (免费额度) |
---
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
</div>

View file

@ -12,7 +12,6 @@ import (
func (s *appState) buildChannelMenuItems() []MenuItem { func (s *appState) buildChannelMenuItems() []MenuItem {
return []MenuItem{ return []MenuItem{
{Label: "Back", Description: "Return to main menu", Action: func() { s.pop() }},
channelItem( channelItem(
"Telegram", "Telegram",
"Telegram bot settings", "Telegram bot settings",
@ -101,10 +100,6 @@ func (s *appState) channelMenu() tview.Primitive {
s.pop() s.pop()
return nil return nil
} }
if event.Rune() == 'q' {
s.pop()
return nil
}
return event return event
}) })
return menu return menu

View file

@ -41,3 +41,15 @@ func bannerView() *tview.TextView {
text.SetBorder(false) text.SetBorder(false)
return text return text
} }
const footerText = "Esc: Back/Exit | Enter: Enter | ←↓↑→ : Move | Space: Select | Tab/Shift+Tab: Switch"
func footerView() *tview.TextView {
text := tview.NewTextView()
text.SetTextAlign(tview.AlignCenter)
text.SetText(footerText)
text.SetBackgroundColor(tview.Styles.MoreContrastBackgroundColor)
text.SetTextColor(tview.Styles.PrimaryTextColor)
text.SetBorder(false)
return text
}

View file

@ -6,11 +6,10 @@ import (
func NewAgentCommand() *cobra.Command { func NewAgentCommand() *cobra.Command {
var ( var (
message string message string
sessionKey string sessionKey string
model string model string
debug bool debug bool
orchestrationEnabled bool
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -18,7 +17,7 @@ func NewAgentCommand() *cobra.Command {
Short: "Interact with the agent directly", Short: "Interact with the agent directly",
Args: cobra.NoArgs, Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return agentCmd(message, sessionKey, model, debug, orchestrationEnabled) return agentCmd(message, sessionKey, model, debug)
}, },
} }
@ -26,7 +25,6 @@ func NewAgentCommand() *cobra.Command {
cmd.Flags().StringVarP(&message, "message", "m", "", "Send a single message (non-interactive mode)") cmd.Flags().StringVarP(&message, "message", "m", "", "Send a single message (non-interactive mode)")
cmd.Flags().StringVarP(&sessionKey, "session", "s", "cli:default", "Session key") cmd.Flags().StringVarP(&sessionKey, "session", "s", "cli:default", "Session key")
cmd.Flags().StringVarP(&model, "model", "", "", "Model to use") cmd.Flags().StringVarP(&model, "model", "", "", "Model to use")
cmd.Flags().BoolVar(&orchestrationEnabled, "orchestration", false, "Enable orchestration mode")
return cmd return cmd
} }

View file

@ -18,7 +18,7 @@ import (
"github.com/sipeed/picoclaw/pkg/providers" "github.com/sipeed/picoclaw/pkg/providers"
) )
func agentCmd(message, sessionKey, model string, debug, orchestrationEnabled bool) error { func agentCmd(message, sessionKey, model string, debug bool) error {
if sessionKey == "" { if sessionKey == "" {
sessionKey = "cli:default" sessionKey = "cli:default"
} }
@ -37,10 +37,6 @@ func agentCmd(message, sessionKey, model string, debug, orchestrationEnabled boo
cfg.Agents.Defaults.ModelName = model cfg.Agents.Defaults.ModelName = model
} }
if orchestrationEnabled {
cfg.Agents.Defaults.Orchestration = true
}
provider, modelID, err := providers.CreateProvider(cfg) provider, modelID, err := providers.CreateProvider(cfg)
if err != nil { if err != nil {
return fmt.Errorf("error creating provider: %w", err) return fmt.Errorf("error creating provider: %w", err)
@ -54,6 +50,7 @@ func agentCmd(message, sessionKey, model string, debug, orchestrationEnabled boo
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
defer msgBus.Close() defer msgBus.Close()
agentLoop := agent.NewAgentLoop(cfg, msgBus, provider) agentLoop := agent.NewAgentLoop(cfg, msgBus, provider)
defer agentLoop.Close()
// Print agent startup info (only for interactive mode) // Print agent startup info (only for interactive mode)
startupInfo := agentLoop.GetStartupInfo() startupInfo := agentLoop.GetStartupInfo()

View file

@ -72,14 +72,14 @@ func authLoginOpenAI(useDeviceCode bool) error {
// If no openai in ModelList, add it // If no openai in ModelList, add it
if !foundOpenAI { if !foundOpenAI {
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{ appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
ModelName: "gpt-5.2", ModelName: "gpt-5.4",
Model: "openai/gpt-5.2", Model: "openai/gpt-5.4",
AuthMethod: "oauth", AuthMethod: "oauth",
}) })
} }
// Update default model to use OpenAI // Update default model to use OpenAI
appCfg.Agents.Defaults.ModelName = "gpt-5.2" appCfg.Agents.Defaults.ModelName = "gpt-5.4"
if err = config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil { if err = config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
return fmt.Errorf("could not update config: %w", err) return fmt.Errorf("could not update config: %w", err)
@ -90,7 +90,7 @@ func authLoginOpenAI(useDeviceCode bool) error {
if cred.AccountID != "" { if cred.AccountID != "" {
fmt.Printf("Account: %s\n", cred.AccountID) fmt.Printf("Account: %s\n", cred.AccountID)
} }
fmt.Println("Default model set to: gpt-5.2") fmt.Println("Default model set to: gpt-5.4")
return nil return nil
} }
@ -318,13 +318,13 @@ func authLoginPasteToken(provider string) error {
} }
if !found { if !found {
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{ appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
ModelName: "gpt-5.2", ModelName: "gpt-5.4",
Model: "openai/gpt-5.2", Model: "openai/gpt-5.4",
AuthMethod: "token", AuthMethod: "token",
}) })
} }
// Update default model // Update default model
appCfg.Agents.Defaults.ModelName = "gpt-5.2" appCfg.Agents.Defaults.ModelName = "gpt-5.4"
} }
if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil { if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
return fmt.Errorf("could not update config: %w", err) return fmt.Errorf("could not update config: %w", err)

View file

@ -1,29 +1,42 @@
package gateway package gateway
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/utils"
) )
func NewGatewayCommand() *cobra.Command { func NewGatewayCommand() *cobra.Command {
var ( var debug bool
debug bool var noTruncate bool
orchestration bool
enableStats bool
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "gateway", Use: "gateway",
Aliases: []string{"g"}, Aliases: []string{"g"},
Short: "Start picoclaw gateway", Short: "Start picoclaw gateway",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRunE: func(_ *cobra.Command, _ []string) error {
if noTruncate && !debug {
return fmt.Errorf("the --no-truncate option can only be used in conjunction with --debug (-d)")
}
if noTruncate {
utils.SetDisableTruncation(true)
logger.Info("String truncation is globally disabled via 'no-truncate' flag")
}
return nil
},
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
return gatewayCmd(debug, orchestration, enableStats) return gatewayCmd(debug)
}, },
} }
cmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging") cmd.Flags().BoolVarP(&debug, "debug", "d", false, "Enable debug logging")
cmd.Flags().BoolVar(&orchestration, "orchestration", false, "Enable subagent orchestration") cmd.Flags().BoolVarP(&noTruncate, "no-truncate", "T", false, "Disable string truncation in debug logs")
cmd.Flags().BoolVar(&enableStats, "stats", false, "Enable stats tracking")
return cmd return cmd
} }

View file

@ -1,23 +1,14 @@
package internal package internal
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
) )
const Logo = "🦞" const Logo = "🦞"
var (
version = "dev"
gitCommit string
buildTime string
goVersion string
)
// GetPicoclawHome returns the picoclaw home directory. // GetPicoclawHome returns the picoclaw home directory.
// Priority: $PICOCLAW_HOME > ~/.picoclaw // Priority: $PICOCLAW_HOME > ~/.picoclaw
func GetPicoclawHome() string { func GetPicoclawHome() string {
@ -40,25 +31,19 @@ func LoadConfig() (*config.Config, error) {
} }
// FormatVersion returns the version string with optional git commit // FormatVersion returns the version string with optional git commit
// Deprecated: Use pkg/config.FormatVersion instead
func FormatVersion() string { func FormatVersion() string {
v := version return config.FormatVersion()
if gitCommit != "" {
v += fmt.Sprintf(" (git: %s)", gitCommit)
}
return v
} }
// FormatBuildInfo returns build time and go version info // FormatBuildInfo returns build time and go version info
// Deprecated: Use pkg/config.FormatBuildInfo instead
func FormatBuildInfo() (string, string) { func FormatBuildInfo() (string, string) {
build := buildTime return config.FormatBuildInfo()
goVer := goVersion
if goVer == "" {
goVer = runtime.Version()
}
return build, goVer
} }
// GetVersion returns the version string // GetVersion returns the version string
// Deprecated: Use pkg/config.GetVersion instead
func GetVersion() string { func GetVersion() string {
return version return config.GetVersion()
} }

View file

@ -1,6 +1,14 @@
package onboard package onboard
import "github.com/spf13/cobra" import (
"embed"
"github.com/spf13/cobra"
)
//go:generate cp -r ../../../../workspace .
//go:embed workspace
var embeddedFiles embed.FS
func NewOnboardCommand() *cobra.Command { func NewOnboardCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{

View file

@ -6,6 +6,7 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/auth" "github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/config"
) )
func statusCmd() { func statusCmd() {
@ -18,8 +19,8 @@ func statusCmd() {
configPath := internal.GetConfigPath() configPath := internal.GetConfigPath()
fmt.Printf("%s picoclaw Status\n", internal.Logo) fmt.Printf("%s picoclaw Status\n", internal.Logo)
fmt.Printf("Version: %s\n", internal.FormatVersion()) fmt.Printf("Version: %s\n", config.FormatVersion())
build, _ := internal.FormatBuildInfo() build, _ := config.FormatBuildInfo()
if build != "" { if build != "" {
fmt.Printf("Build: %s\n", build) fmt.Printf("Build: %s\n", build)
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
) )
func NewVersionCommand() *cobra.Command { func NewVersionCommand() *cobra.Command {
@ -22,8 +23,8 @@ func NewVersionCommand() *cobra.Command {
} }
func printVersion() { func printVersion() {
fmt.Printf("%s picoclaw %s\n", internal.Logo, internal.FormatVersion()) fmt.Printf("%s picoclaw %s\n", internal.Logo, config.FormatVersion())
build, goVer := internal.FormatBuildInfo() build, goVer := config.FormatBuildInfo()
if build != "" { if build != "" {
fmt.Printf(" Build: %s\n", build) fmt.Printf(" Build: %s\n", build)
} }

View file

@ -22,15 +22,16 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/skills" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/skills"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/status" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/status"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/version" "github.com/sipeed/picoclaw/cmd/picoclaw/internal/version"
"github.com/sipeed/picoclaw/pkg/config"
) )
func NewPicoclawCommand() *cobra.Command { func NewPicoclawCommand() *cobra.Command {
short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, internal.GetVersion()) short := fmt.Sprintf("%s picoclaw - Personal AI Assistant v%s\n\n", internal.Logo, config.GetVersion())
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "picoclaw", Use: "picoclaw",
Short: short, Short: short,
Example: "picoclaw list", Example: "picoclaw version",
} }
cmd.AddCommand( cmd.AddCommand(

View file

@ -3,7 +3,7 @@
"defaults": { "defaults": {
"workspace": "~/.picoclaw/workspace", "workspace": "~/.picoclaw/workspace",
"restrict_to_workspace": true, "restrict_to_workspace": true,
"model_name": "gpt4", "model_name": "gpt-5.4",
"max_tokens": 8192, "max_tokens": 8192,
"temperature": 0.7, "temperature": 0.7,
"max_tool_iterations": 20, "max_tool_iterations": 20,
@ -13,8 +13,8 @@
}, },
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key", "api_key": "sk-your-openai-key",
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
}, },
@ -36,14 +36,19 @@
"api_key": "sk-your-deepseek-key" "api_key": "sk-your-deepseek-key"
}, },
{ {
"model_name": "loadbalanced-gpt4", "model_name": "longcat",
"model": "openai/gpt-5.2", "model": "longcat/LongCat-Flash-Thinking",
"api_key": "your-longcat-api-key"
},
{
"model_name": "loadbalanced-gpt-5.4",
"model": "openai/gpt-5.4",
"api_key": "sk-key1", "api_key": "sk-key1",
"api_base": "https://api1.example.com/v1" "api_base": "https://api1.example.com/v1"
}, },
{ {
"model_name": "loadbalanced-gpt4", "model_name": "loadbalanced-gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-key2", "api_key": "sk-key2",
"api_base": "https://api2.example.com/v1" "api_base": "https://api2.example.com/v1"
} }
@ -194,8 +199,13 @@
"nickserv_password": "", "nickserv_password": "",
"sasl_user": "", "sasl_user": "",
"sasl_password": "", "sasl_password": "",
"channels": ["#mychannel"], "channels": [
"request_caps": ["server-time", "message-tags"], "#mychannel"
],
"request_caps": [
"server-time",
"message-tags"
],
"allow_from": [], "allow_from": [],
"group_trigger": { "group_trigger": {
"mention_only": true "mention_only": true
@ -269,6 +279,10 @@
"avian": { "avian": {
"api_key": "", "api_key": "",
"api_base": "https://api.avian.io/v1" "api_base": "https://api.avian.io/v1"
},
"longcat": {
"api_key": "",
"api_base": "https://api.longcat.chat/openai"
} }
}, },
"tools": { "tools": {
@ -279,6 +293,9 @@
"brave": { "brave": {
"enabled": false, "enabled": false,
"api_key": "YOUR_BRAVE_API_KEY", "api_key": "YOUR_BRAVE_API_KEY",
"api_keys": [
"YOUR_BRAVE_API_KEY"
],
"max_results": 5 "max_results": 5
}, },
"tavily": { "tavily": {
@ -293,7 +310,10 @@
}, },
"perplexity": { "perplexity": {
"enabled": false, "enabled": false,
"api_key": "", "api_key": "pplx-xxx",
"api_keys": [
"pplx-xxx"
],
"max_results": 5 "max_results": 5
}, },
"searxng": { "searxng": {
@ -316,6 +336,13 @@
}, },
"mcp": { "mcp": {
"enabled": false, "enabled": false,
"discovery": {
"enabled": false,
"ttl": 5,
"max_search_results": 5,
"use_bm25": true,
"use_regex": false
},
"servers": { "servers": {
"context7": { "context7": {
"enabled": false, "enabled": false,
@ -459,6 +486,9 @@
"enabled": false, "enabled": false,
"monitor_usb": true "monitor_usb": true
}, },
"voice": {
"echo_transcription": false
},
"gateway": { "gateway": {
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 18790 "port": 18790

View file

@ -19,7 +19,7 @@ services:
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# PicoClaw Gateway (Long-running Bot) # PicoClaw Gateway (Long-running Bot)
# docker compose -f docker/docker-compose.yml up picoclaw-gateway # docker compose -f docker/docker-compose.yml --profile gateway up
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
picoclaw-gateway: picoclaw-gateway:
image: docker.io/sipeed/picoclaw:latest image: docker.io/sipeed/picoclaw:latest
@ -32,3 +32,21 @@ services:
# - "host.docker.internal:host-gateway" # - "host.docker.internal:host-gateway"
volumes: volumes:
- ./data:/root/.picoclaw - ./data:/root/.picoclaw
# ─────────────────────────────────────────────
# PicoClaw Launcher (Web Console + Gateway)
# docker compose -f docker/docker-compose.yml --profile launcher up
# ─────────────────────────────────────────────
picoclaw-launcher:
image: docker.io/sipeed/picoclaw:launcher
container_name: picoclaw-launcher
restart: on-failure
profiles:
- launcher
environment:
- PICOCLAW_GATEWAY_HOST=0.0.0.0
ports:
- "127.0.0.1:18800:18800"
- "127.0.0.1:18790:18790"
volumes:
- ./data:/root/.picoclaw

View file

@ -22,7 +22,8 @@ Add this to `config.json`:
"enabled": true, "enabled": true,
"text": "Thinking..." "text": "Thinking..."
}, },
"reasoning_channel_id": "" "reasoning_channel_id": "",
"message_format": "richtext"
} }
} }
} }
@ -42,10 +43,12 @@ Add this to `config.json`:
| group_trigger | object | No | Group trigger strategy (`mention_only` / `prefixes`) | | group_trigger | object | No | Group trigger strategy (`mention_only` / `prefixes`) |
| placeholder | object | No | Placeholder message config | | placeholder | object | No | Placeholder message config |
| reasoning_channel_id | string | No | Target channel for reasoning output | | reasoning_channel_id | string | No | Target channel for reasoning output |
| message_format | string | No | Output format: `"richtext"` (default) renders markdown as HTML; `"plain"` sends plain text only |
## 3. Currently Supported ## 3. Currently Supported
- Text message send/receive - Text message send/receive with markdown rendering (bold, italic, headers, code blocks, etc.)
- Configurable message format (`richtext` / `plain`)
- Incoming image/audio/video/file download (MediaStore first, local path fallback) - Incoming image/audio/video/file download (MediaStore first, local path fallback)
- Incoming audio normalization into existing transcription flow (`[audio: ...]`) - Incoming audio normalization into existing transcription flow (`[audio: ...]`)
- Outgoing image/audio/video/file upload and send - Outgoing image/audio/video/file upload and send

View file

@ -66,7 +66,7 @@ Problem: Agent needs to know both `provider` and `model`, adding complexity.
Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design: Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
1. **Model-centric**: Users care about models, not providers 1. **Model-centric**: Users care about models, not providers
2. **Protocol prefix**: Use `protocol/model_name` format, e.g., `openai/gpt-5.2`, `anthropic/claude-sonnet-4.6` 2. **Protocol prefix**: Use `protocol/model_name` format, e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4.6`
3. **Configuration-driven**: Adding new Providers only requires config changes, no code changes 3. **Configuration-driven**: Adding new Providers only requires config changes, no code changes
### 2.2 New Configuration Structure ### 2.2 New Configuration Structure
@ -81,8 +81,8 @@ Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
"api_key": "sk-xxx" "api_key": "sk-xxx"
}, },
{ {
"model_name": "gpt-5.2", "model_name": "gpt-5.4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-xxx" "api_key": "sk-xxx"
}, },
{ {
@ -128,7 +128,7 @@ type Config struct {
type ModelConfig struct { type ModelConfig struct {
// Required // Required
ModelName string `json:"model_name"` // user-facing name (alias) ModelName string `json:"model_name"` // user-facing name (alias)
Model string `json:"model"` // protocol/model, e.g., openai/gpt-5.2 Model string `json:"model"` // protocol/model, e.g., openai/gpt-5.4
// Common config // Common config
APIBase string `json:"api_base,omitempty"` APIBase string `json:"api_base,omitempty"`
@ -180,7 +180,7 @@ Identify protocol via prefix in `model` field:
"model": "deepseek-chat" "model": "deepseek-chat"
}, },
"coder": { "coder": {
"model": "gpt-5.2", "model": "gpt-5.4",
"system_prompt": "You are a coding assistant..." "system_prompt": "You are a coding assistant..."
}, },
"translator": { "translator": {
@ -200,7 +200,7 @@ Each Agent only needs to specify `model` (corresponds to `model_name` in `model_
model_list: model_list:
- model_name: gpt-4o - model_name: gpt-4o
litellm_params: litellm_params:
model: openai/gpt-5.2 model: openai/gpt-5.4
api_key: xxx api_key: xxx
- model_name: my-custom - model_name: my-custom
litellm_params: litellm_params:

View file

@ -40,7 +40,7 @@ The new `model_list` configuration offers several advantages:
"agents": { "agents": {
"defaults": { "defaults": {
"provider": "openai", "provider": "openai",
"model": "gpt-5.2" "model": "gpt-5.4"
} }
} }
} }
@ -53,7 +53,7 @@ The new `model_list` configuration offers several advantages:
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-your-openai-key", "api_key": "sk-your-openai-key",
"api_base": "https://api.openai.com/v1" "api_base": "https://api.openai.com/v1"
}, },
@ -82,7 +82,7 @@ The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
| Prefix | Description | Example | | Prefix | Description | Example |
|--------|-------------|---------| |--------|-------------|---------|
| `openai/` | OpenAI API (default) | `openai/gpt-5.2` | | `openai/` | OpenAI API (default) | `openai/gpt-5.4` |
| `anthropic/` | Anthropic API | `anthropic/claude-opus-4` | | `anthropic/` | Anthropic API | `anthropic/claude-opus-4` |
| `antigravity/` | Google via Antigravity OAuth | `antigravity/gemini-2.0-flash` | | `antigravity/` | Google via Antigravity OAuth | `antigravity/gemini-2.0-flash` |
| `gemini/` | Google Gemini API | `gemini/gemini-2.0-flash-exp` | | `gemini/` | Google Gemini API | `gemini/gemini-2.0-flash-exp` |
@ -109,7 +109,7 @@ The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
| Field | Required | Description | | Field | Required | Description |
|-------|----------|-------------| |-------|----------|-------------|
| `model_name` | Yes | User-facing alias for the model | | `model_name` | Yes | User-facing alias for the model |
| `model` | Yes | Protocol and model identifier (e.g., `openai/gpt-5.2`) | | `model` | Yes | Protocol and model identifier (e.g., `openai/gpt-5.4`) |
| `api_base` | No | API endpoint URL | | `api_base` | No | API endpoint URL |
| `api_key` | No* | API authentication key | | `api_key` | No* | API authentication key |
| `proxy` | No | HTTP proxy URL | | `proxy` | No | HTTP proxy URL |
@ -130,19 +130,19 @@ Configure multiple endpoints for the same model to distribute load:
"model_list": [ "model_list": [
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-key1", "api_key": "sk-key1",
"api_base": "https://api1.example.com/v1" "api_base": "https://api1.example.com/v1"
}, },
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-key2", "api_key": "sk-key2",
"api_base": "https://api2.example.com/v1" "api_base": "https://api2.example.com/v1"
}, },
{ {
"model_name": "gpt4", "model_name": "gpt4",
"model": "openai/gpt-5.2", "model": "openai/gpt-5.4",
"api_key": "sk-key3", "api_key": "sk-key3",
"api_base": "https://api3.example.com/v1" "api_base": "https://api3.example.com/v1"
} }

View file

@ -7,11 +7,21 @@ PicoClaw's tools configuration is located in the `tools` field of `config.json`.
```json ```json
{ {
"tools": { "tools": {
"web": { ... }, "web": {
"mcp": { ... }, ...
"exec": { ... }, },
"cron": { ... }, "mcp": {
"skills": { ... } ...
},
"exec": {
...
},
"cron": {
...
},
"skills": {
...
}
} }
} }
``` ```
@ -23,7 +33,7 @@ Web tools are used for web search and fetching.
### Brave ### Brave
| Config | Type | Default | Description | | Config | Type | Default | Description |
| ------------- | ------ | ------- | ------------------------- | |---------------|--------|---------|---------------------------|
| `enabled` | bool | false | Enable Brave search | | `enabled` | bool | false | Enable Brave search |
| `api_key` | string | - | Brave Search API key | | `api_key` | string | - | Brave Search API key |
| `max_results` | int | 5 | Maximum number of results | | `max_results` | int | 5 | Maximum number of results |
@ -31,14 +41,14 @@ Web tools are used for web search and fetching.
### DuckDuckGo ### DuckDuckGo
| Config | Type | Default | Description | | Config | Type | Default | Description |
| ------------- | ---- | ------- | ------------------------- | |---------------|------|---------|---------------------------|
| `enabled` | bool | true | Enable DuckDuckGo search | | `enabled` | bool | true | Enable DuckDuckGo search |
| `max_results` | int | 5 | Maximum number of results | | `max_results` | int | 5 | Maximum number of results |
### Perplexity ### Perplexity
| Config | Type | Default | Description | | Config | Type | Default | Description |
| ------------- | ------ | ------- | ------------------------- | |---------------|--------|---------|---------------------------|
| `enabled` | bool | false | Enable Perplexity search | | `enabled` | bool | false | Enable Perplexity search |
| `api_key` | string | - | Perplexity API key | | `api_key` | string | - | Perplexity API key |
| `max_results` | int | 5 | Maximum number of results | | `max_results` | int | 5 | Maximum number of results |
@ -48,7 +58,7 @@ Web tools are used for web search and fetching.
The exec tool is used to execute shell commands. The exec tool is used to execute shell commands.
| Config | Type | Default | Description | | Config | Type | Default | Description |
| ---------------------- | ----- | ------- | ------------------------------------------ | |------------------------|-------|---------|--------------------------------------------|
| `enable_deny_patterns` | bool | true | Enable default dangerous command blocking | | `enable_deny_patterns` | bool | true | Enable default dangerous command blocking |
| `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) | | `custom_deny_patterns` | array | [] | Custom deny patterns (regular expressions) |
@ -81,7 +91,10 @@ By default, PicoClaw blocks the following dangerous commands:
"tools": { "tools": {
"exec": { "exec": {
"enable_deny_patterns": true, "enable_deny_patterns": true,
"custom_deny_patterns": ["\\brm\\s+-r\\b", "\\bkillall\\s+python"] "custom_deny_patterns": [
"\\brm\\s+-r\\b",
"\\bkillall\\s+python"
]
} }
} }
} }
@ -92,24 +105,47 @@ By default, PicoClaw blocks the following dangerous commands:
The cron tool is used for scheduling periodic tasks. The cron tool is used for scheduling periodic tasks.
| Config | Type | Default | Description | | Config | Type | Default | Description |
| ---------------------- | ---- | ------- | ---------------------------------------------- | |------------------------|------|---------|------------------------------------------------|
| `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit | | `exec_timeout_minutes` | int | 5 | Execution timeout in minutes, 0 means no limit |
## MCP Tool ## MCP Tool
The MCP tool enables integration with external Model Context Protocol servers. The MCP tool enables integration with external Model Context Protocol servers.
### Tool Discovery (Lazy Loading)
When connecting to multiple MCP servers, exposing hundreds of tools simultaneously can exhaust the LLM's context window
and increase API costs. The **Discovery** feature solves this by keeping MCP tools *hidden* by default.
Instead of loading all tools, the LLM is provided with a lightweight search tool (using BM25 keyword matching or Regex).
When the LLM needs a specific capability, it searches the hidden library. Matching tools are then temporarily "unlocked"
and injected into the context for a configured number of turns (`ttl`).
### Global Config ### Global Config
| Config | Type | Default | Description | | Config | Type | Default | Description |
| --------- | ------ | ------- | ----------------------------------- | |-------------|--------|---------|----------------------------------------------|
| `enabled` | bool | false | Enable MCP integration globally | | `enabled` | bool | false | Enable MCP integration globally |
| `servers` | object | `{}` | Map of server name to server config | | `discovery` | object | `{}` | Configuration for Tool Discovery (see below) |
| `servers` | object | `{}` | Map of server name to server config |
### Discovery Config (`discovery`)
| Config | Type | Default | Description |
|----------------------|------|---------|-----------------------------------------------------------------------------------------------------------------------------------|
| `enabled` | bool | false | If true, MCP tools are hidden and loaded on-demand via search. If false, all tools are loaded |
| `ttl` | int | 5 | Number of conversational turns a discovered tool remains unlocked |
| `max_search_results` | int | 5 | Maximum number of tools returned per search query |
| `use_bm25` | bool | true | Enable the natural language/keyword search tool (`tool_search_tool_bm25`). **Warning**: consumes more resources than regex search |
| `use_regex` | bool | false | Enable the regex pattern search tool (`tool_search_tool_regex`) |
> **Note:** If `discovery.enabled` is `true`, you MUST enable at least one search engine (`use_bm25` or `use_regex`),
> otherwise the application will fail to start.
### Per-Server Config ### Per-Server Config
| Config | Type | Required | Description | | Config | Type | Required | Description |
| ---------- | ------ | -------- | ------------------------------------------ | |------------|--------|----------|--------------------------------------------|
| `enabled` | bool | yes | Enable this MCP server | | `enabled` | bool | yes | Enable this MCP server |
| `type` | string | no | Transport type: `stdio`, `sse`, `http` | | `type` | string | no | Transport type: `stdio`, `sse`, `http` |
| `command` | string | stdio | Executable command for stdio transport | | `command` | string | stdio | Executable command for stdio transport |
@ -122,8 +158,8 @@ The MCP tool enables integration with external Model Context Protocol servers.
### Transport Behavior ### Transport Behavior
- If `type` is omitted, transport is auto-detected: - If `type` is omitted, transport is auto-detected:
- `url` is set → `sse` - `url` is set → `sse`
- `command` is set → `stdio` - `command` is set → `stdio`
- `http` and `sse` both use `url` + optional `headers`. - `http` and `sse` both use `url` + optional `headers`.
- `env` and `env_file` are only applied to `stdio` servers. - `env` and `env_file` are only applied to `stdio` servers.
@ -140,7 +176,11 @@ The MCP tool enables integration with external Model Context Protocol servers.
"filesystem": { "filesystem": {
"enabled": true, "enabled": true,
"command": "npx", "command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] "args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/tmp"
]
} }
} }
} }
@ -170,20 +210,76 @@ The MCP tool enables integration with external Model Context Protocol servers.
} }
``` ```
#### 3) Massive MCP setup with Tool Discovery enabled
*In this example, the LLM will only see the `tool_search_tool_bm25`. It will search and unlock Github or Postgres tools
dynamically only when requested by the user.*
```json
{
"tools": {
"mcp": {
"enabled": true,
"discovery": {
"enabled": true,
"ttl": 5,
"max_search_results": 5,
"use_bm25": true,
"use_regex": false
},
"servers": {
"github": {
"enabled": true,
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-github"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_TOKEN"
}
},
"postgres": {
"enabled": true,
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"postgresql://user:password@localhost/dbname"
]
},
"slack": {
"enabled": true,
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-slack"
],
"env": {
"SLACK_BOT_TOKEN": "YOUR_SLACK_BOT_TOKEN",
"SLACK_TEAM_ID": "YOUR_SLACK_TEAM_ID"
}
}
}
}
}
}
```
## Skills Tool ## Skills Tool
The skills tool configures skill discovery and installation via registries like ClawHub. The skills tool configures skill discovery and installation via registries like ClawHub.
### Registries ### Registries
| Config | Type | Default | Description | | Config | Type | Default | Description |
| ---------------------------------- | ------ | -------------------- | ----------------------- | |------------------------------------|--------|----------------------|----------------------------------------------|
| `registries.clawhub.enabled` | bool | true | Enable ClawHub registry | | `registries.clawhub.enabled` | bool | true | Enable ClawHub registry |
| `registries.clawhub.base_url` | string | `https://clawhub.ai` | ClawHub base URL | | `registries.clawhub.base_url` | string | `https://clawhub.ai` | ClawHub base URL |
| `registries.clawhub.auth_token` | string | `""` | Optional Bearer token for higher rate limits | | `registries.clawhub.auth_token` | string | `""` | Optional Bearer token for higher rate limits |
| `registries.clawhub.search_path` | string | `/api/v1/search` | Search API path | | `registries.clawhub.search_path` | string | `/api/v1/search` | Search API path |
| `registries.clawhub.skills_path` | string | `/api/v1/skills` | Skills API path | | `registries.clawhub.skills_path` | string | `/api/v1/skills` | Skills API path |
| `registries.clawhub.download_path` | string | `/api/v1/download` | Download API path | | `registries.clawhub.download_path` | string | `/api/v1/download` | Download API path |
### Configuration Example ### Configuration Example
@ -217,4 +313,5 @@ For example:
- `PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES=10` - `PICOCLAW_TOOLS_CRON_EXEC_TIMEOUT_MINUTES=10`
- `PICOCLAW_TOOLS_MCP_ENABLED=true` - `PICOCLAW_TOOLS_MCP_ENABLED=true`
Note: Nested map-style config (for example `tools.mcp.servers.<name>.*`) is configured in `config.json` rather than environment variables. Note: Nested map-style config (for example `tools.mcp.servers.<name>.*`) is configured in `config.json` rather than
environment variables.

View file

@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"encoding/hex" "encoding/hex"
"regexp"
"strconv" "strconv"
"strings" "strings"
"sync/atomic" "sync/atomic"
@ -32,6 +33,9 @@ func init() {
uniqueIDPrefix = hex.EncodeToString(b[:]) uniqueIDPrefix = hex.EncodeToString(b[:])
} }
// audioAnnotationRe matches audio/voice annotations injected by channels (e.g. [voice], [audio: file.ogg]).
var audioAnnotationRe = regexp.MustCompile(`\[(voice|audio)(?::[^\]]*)?\]`)
// uniqueID generates a process-unique ID using a random prefix and an atomic counter. // uniqueID generates a process-unique ID using a random prefix and an atomic counter.
// This ID is intended for internal correlation (e.g. media scope keys) and is NOT // This ID is intended for internal correlation (e.g. media scope keys) and is NOT
// cryptographically secure — it must not be used in contexts where unpredictability matters. // cryptographically secure — it must not be used in contexts where unpredictability matters.
@ -285,10 +289,10 @@ func (c *BaseChannel) HandleMessage(
} }
} }
// Placeholder — independent pipeline. // Placeholder — independent pipeline.
// Skip for DraftSender channels: the streaming draft bubble serves // Skip when the message contains audio: the agent will send the
// as the placeholder, and sendMessage on final response replaces it. // placeholder after transcription completes, so the user sees
// Sending both a placeholder AND drafts causes duplicate chat bubbles. // "Thinking…" only once the voice has been processed.
if _, isDrafter := c.owner.(DraftSender); !isDrafter { if !audioAnnotationRe.MatchString(content) {
if pc, ok := c.owner.(PlaceholderCapable); ok { if pc, ok := c.owner.(PlaceholderCapable); ok {
if phID, err := pc.SendPlaceholder(ctx, chatID); err == nil && phID != "" { if phID, err := pc.SendPlaceholder(ctx, chatID); err == nil && phID != "" {
c.placeholderRecorder.RecordPlaceholder(c.name, chatID, phID) c.placeholderRecorder.RecordPlaceholder(c.name, chatID, phID)

View file

@ -10,6 +10,7 @@ import (
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client" "github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
dinglog "github.com/open-dingtalk/dingtalk-stream-sdk-go/logger"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
@ -39,6 +40,9 @@ func NewDingTalkChannel(cfg config.DingTalkConfig, messageBus *bus.MessageBus) (
return nil, fmt.Errorf("dingtalk client_id and client_secret are required") return nil, fmt.Errorf("dingtalk client_id and client_secret are required")
} }
// Set the logger for the Stream SDK
dinglog.SetLogger(logger.NewLogger("dingtalk"))
base := channels.NewBaseChannel("dingtalk", cfg, messageBus, cfg.AllowFrom, base := channels.NewBaseChannel("dingtalk", cfg, messageBus, cfg.AllowFrom,
channels.WithMaxMessageLength(20000), channels.WithMaxMessageLength(20000),
channels.WithGroupTrigger(cfg.GroupTrigger), channels.WithGroupTrigger(cfg.GroupTrigger),

View file

@ -32,6 +32,10 @@ const (
lineBotInfoEndpoint = lineAPIBase + "/info" lineBotInfoEndpoint = lineAPIBase + "/info"
lineLoadingEndpoint = lineAPIBase + "/chat/loading/start" lineLoadingEndpoint = lineAPIBase + "/chat/loading/start"
lineReplyTokenMaxAge = 25 * time.Second lineReplyTokenMaxAge = 25 * time.Second
// Limit request body to prevent memory exhaustion (DoS).
// LINE webhook payloads are typically a few KB; 1 MiB is generous.
maxWebhookBodySize = 1 << 20 // 1 MiB
) )
type replyTokenEntry struct { type replyTokenEntry struct {
@ -166,7 +170,7 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
body, err := io.ReadAll(r.Body) body, err := io.ReadAll(io.LimitReader(r.Body, maxWebhookBodySize+1))
if err != nil { if err != nil {
logger.ErrorCF("line", "Failed to read request body", map[string]any{ logger.ErrorCF("line", "Failed to read request body", map[string]any{
"error": err.Error(), "error": err.Error(),
@ -174,6 +178,11 @@ func (c *LINEChannel) webhookHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad request", http.StatusBadRequest) http.Error(w, "Bad request", http.StatusBadRequest)
return return
} }
if int64(len(body)) > maxWebhookBodySize {
logger.WarnC("line", "Webhook request body too large, rejected")
http.Error(w, "Request entity too large", http.StatusRequestEntityTooLarge)
return
}
signature := r.Header.Get("X-Line-Signature") signature := r.Header.Get("X-Line-Signature")
if !c.verifySignature(body, signature) { if !c.verifySignature(body, signature) {

View file

@ -12,5 +12,6 @@ func BuiltinDefinitions() []Definition {
listCommand(), listCommand(),
switchCommand(), switchCommand(),
checkCommand(), checkCommand(),
clearCommand(),
} }
} }

View file

@ -13,4 +13,5 @@ type Runtime struct {
GetEnabledChannels func() []string GetEnabledChannels func() []string
SwitchModel func(value string) (oldModel string, err error) SwitchModel func(value string) (oldModel string, err error)
SwitchChannel func(value string) error SwitchChannel func(value string) error
ClearHistory func() error
} }

View file

@ -227,18 +227,18 @@ type AgentDefaults struct {
ModelName string `json:"model_name,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"` ModelName string `json:"model_name,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL_NAME"`
Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead Model string `json:"model" env:"PICOCLAW_AGENTS_DEFAULTS_MODEL"` // Deprecated: use model_name instead
ModelFallbacks []string `json:"model_fallbacks,omitempty"` ModelFallbacks []string `json:"model_fallbacks,omitempty"`
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
PlanModel string `json:"plan_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_PLAN_MODEL"` PlanModel string `json:"plan_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_PLAN_MODEL"`
PlanModelFallbacks []string `json:"plan_model_fallbacks,omitempty"` PlanModelFallbacks []string `json:"plan_model_fallbacks,omitempty"`
ImageModel string `json:"image_model,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_IMAGE_MODEL"`
ImageModelFallbacks []string `json:"image_model_fallbacks,omitempty"`
MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"` MaxTokens int `json:"max_tokens" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOKENS"`
Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"` Temperature *float64 `json:"temperature,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_TEMPERATURE"`
MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"` MaxToolIterations int `json:"max_tool_iterations" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_TOOL_ITERATIONS"`
TaskReminderInterval int `json:"task_reminder_interval" env:"PICOCLAW_AGENTS_DEFAULTS_TASK_REMINDER_INTERVAL"`
Orchestration bool `json:"orchestration,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_ORCHESTRATION"`
SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"` SummarizeMessageThreshold int `json:"summarize_message_threshold" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_MESSAGE_THRESHOLD"`
SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"` SummarizeTokenPercent int `json:"summarize_token_percent" env:"PICOCLAW_AGENTS_DEFAULTS_SUMMARIZE_TOKEN_PERCENT"`
MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"` MaxMediaSize int `json:"max_media_size,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_MAX_MEDIA_SIZE"`
TaskReminderInterval int `json:"task_reminder_interval" env:"PICOCLAW_AGENTS_DEFAULTS_TASK_REMINDER_INTERVAL"`
Orchestration bool `json:"orchestration,omitempty" env:"PICOCLAW_AGENTS_DEFAULTS_ORCHESTRATION"`
Routing *RoutingConfig `json:"routing,omitempty"` Routing *RoutingConfig `json:"routing,omitempty"`
} }
@ -304,19 +304,20 @@ type WhatsAppConfig struct {
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WHATSAPP_ALLOW_FROM"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WHATSAPP_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_WHATSAPP_REASONING_CHANNEL_ID"`
} }
type TelegramConfig struct { type TelegramConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"` Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"` Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_TELEGRAM_BASE_URL"` BaseURL string `json:"base_url" env:"PICOCLAW_CHANNELS_TELEGRAM_BASE_URL"`
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"` Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
WebAppURL string `json:"web_app_url" env:"PICOCLAW_CHANNELS_TELEGRAM_WEB_APP_URL"`
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
Typing TypingConfig `json:"typing,omitempty"` Typing TypingConfig `json:"typing,omitempty"`
Placeholder PlaceholderConfig `json:"placeholder,omitempty"` Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
WebAppURL string `json:"web_app_url" env:"PICOCLAW_CHANNELS_TELEGRAM_WEB_APP_URL"`
SubagentThreadID int `json:"subagent_thread_id,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_SUBAGENT_THREAD_ID"` SubagentThreadID int `json:"subagent_thread_id,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_SUBAGENT_THREAD_ID"`
HeartbeatThreadID int `json:"heartbeat_thread_id,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_HEARTBEAT_THREAD_ID"` HeartbeatThreadID int `json:"heartbeat_thread_id,omitempty" env:"PICOCLAW_CHANNELS_TELEGRAM_HEARTBEAT_THREAD_ID"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"`
} }
type FeishuConfig struct { type FeishuConfig struct {
@ -394,7 +395,7 @@ type MatrixConfig struct {
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MATRIX_ALLOW_FROM"` AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_MATRIX_ALLOW_FROM"`
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
Placeholder PlaceholderConfig `json:"placeholder,omitempty"` Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_MATRIX_REASONING_CHANNEL_ID"`
} }
type LINEConfig struct { type LINEConfig struct {
@ -609,12 +610,13 @@ type ModelConfig struct {
AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token AuthMethod string `json:"auth_method,omitempty"` // Authentication method: oauth, token
ConnectMode string `json:"connect_mode,omitempty"` // Connection mode: stdio, grpc ConnectMode string `json:"connect_mode,omitempty"` // Connection mode: stdio, grpc
Workspace string `json:"workspace,omitempty"` // Workspace path for CLI-based providers Workspace string `json:"workspace,omitempty"` // Workspace path for CLI-based providers
// Optional optimizations // Optional optimizations
RPM int `json:"rpm,omitempty"` // Requests per minute limit RPM int `json:"rpm,omitempty"` // Requests per minute limit
MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens") MaxTokensField string `json:"max_tokens_field,omitempty"` // Field name for max tokens (e.g., "max_completion_tokens")
Stream *bool `json:"stream,omitempty"` // Use SSE streaming (default: protocol-dependent)
RequestTimeout int `json:"request_timeout,omitempty"` RequestTimeout int `json:"request_timeout,omitempty"`
ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive ThinkingLevel string `json:"thinking_level,omitempty"` // Extended thinking: off|low|medium|high|xhigh|adaptive
Stream *bool `json:"stream,omitempty"` // Use SSE streaming (default: protocol-dependent)
} }
// Validate checks if the ModelConfig has all required fields. // Validate checks if the ModelConfig has all required fields.

View file

@ -407,6 +407,23 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
}, true }, true
}, },
}, },
{
providerNames: []string{"longcat"},
protocol: "longcat",
buildConfig: func(p ProvidersConfig) (ModelConfig, bool) {
if p.LongCat.APIKey == "" && p.LongCat.APIBase == "" {
return ModelConfig{}, false
}
return ModelConfig{
ModelName: "longcat",
Model: "longcat/LongCat-Flash-Thinking",
APIKey: p.LongCat.APIKey,
APIBase: p.LongCat.APIBase,
Proxy: p.LongCat.Proxy,
RequestTimeout: p.LongCat.RequestTimeout,
}, true
},
},
} }
// Process each provider migration // Process each provider migration

View file

@ -2,7 +2,6 @@ package health
import ( import (
"context" "context"
"crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps" "maps"
@ -13,7 +12,6 @@ import (
type Server struct { type Server struct {
server *http.Server server *http.Server
mux *http.ServeMux
mu sync.RWMutex mu sync.RWMutex
ready bool ready bool
checks map[string]Check checks map[string]Check
@ -36,7 +34,6 @@ type StatusResponse struct {
func NewServer(host string, port int) *Server { func NewServer(host string, port int) *Server {
mux := http.NewServeMux() mux := http.NewServeMux()
s := &Server{ s := &Server{
mux: mux,
ready: false, ready: false,
checks: make(map[string]Check), checks: make(map[string]Check),
startTime: time.Now(), startTime: time.Now(),
@ -49,34 +46,13 @@ func NewServer(host string, port int) *Server {
s.server = &http.Server{ s.server = &http.Server{
Addr: addr, Addr: addr,
Handler: mux, Handler: mux,
ReadTimeout: 30 * time.Second, ReadTimeout: 5 * time.Second,
WriteTimeout: 30 * time.Second, WriteTimeout: 5 * time.Second,
} }
return s return s
} }
// Mux returns the underlying ServeMux so additional routes can be registered.
func (s *Server) Mux() *http.ServeMux {
return s.mux
}
// StartTLS starts the server with TLS using the provided certificate and key files.
func (s *Server) StartTLS(certFile, keyFile string) error {
s.mu.Lock()
s.ready = true
s.mu.Unlock()
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return fmt.Errorf("failed to load TLS cert: %w", err)
}
s.server.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
return s.server.ListenAndServeTLS("", "")
}
func (s *Server) Start() error { func (s *Server) Start() error {
s.mu.Lock() s.mu.Lock()
s.ready = true s.ready = true

28
pkg/health/server_ext.go Normal file
View file

@ -0,0 +1,28 @@
package health
import (
"crypto/tls"
"fmt"
"net/http"
)
// Mux returns the underlying ServeMux so additional routes can be registered.
func (s *Server) Mux() *http.ServeMux {
return s.server.Handler.(*http.ServeMux)
}
// StartTLS starts the server with TLS using the provided certificate and key files.
func (s *Server) StartTLS(certFile, keyFile string) error {
s.mu.Lock()
s.ready = true
s.mu.Unlock()
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
return fmt.Errorf("failed to load TLS cert: %w", err)
}
s.server.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
return s.server.ListenAndServeTLS("", "")
}

View file

@ -86,14 +86,14 @@ func (s *JSONLStore) metaPath(key string) string {
// sanitizeKey converts a session key to a safe filename component. // sanitizeKey converts a session key to a safe filename component.
// Mirrors pkg/session.sanitizeFilename so that migration paths match. // Mirrors pkg/session.sanitizeFilename so that migration paths match.
// // Replaces ':' with '_' (session key separator) and '/' and '\' with '_'
// Note: this is a lossy mapping — "telegram:123" and "telegram_123" // so composite IDs (e.g. Telegram forum "chatID/threadID", Slack "channel/thread_ts")
// both produce the same filename. This is an intentional tradeoff: // do not create subdirectories or break on Windows.
// keys with colons (e.g. from channels) are by far the common case,
// and a bidirectional encoding (like URL-encoding) would complicate
// file listings and debugging.
func sanitizeKey(key string) string { func sanitizeKey(key string) string {
return strings.ReplaceAll(key, ":", "_") s := strings.ReplaceAll(key, ":", "_")
s = strings.ReplaceAll(s, "/", "_")
s = strings.ReplaceAll(s, "\\", "_")
return s
} }
// readMeta loads the metadata file for a session. // readMeta loads the metadata file for a session.

View file

@ -48,6 +48,12 @@ func MigrateFromJSON(
if !strings.HasSuffix(name, ".json") { if !strings.HasSuffix(name, ".json") {
continue continue
} }
// Skip JSONL metadata files. They are part of the new storage format,
// not legacy session snapshots, and re-importing them would overwrite
// the paired .jsonl history with an empty message list.
if strings.HasSuffix(name, ".meta.json") {
continue
}
// Skip already-migrated files. // Skip already-migrated files.
if strings.HasSuffix(name, ".migrated") { if strings.HasSuffix(name, ".migrated") {
continue continue

View file

@ -4,8 +4,8 @@ var migrateableFiles = []string{
"AGENTS.md", "AGENTS.md",
"SOUL.md", "SOUL.md",
"USER.md", "USER.md",
"TOOLS.md",
"HEARTBEAT.md", "HEARTBEAT.md",
"TOOLS.md",
} }
var migrateableDirs = []string{ var migrateableDirs = []string{

View file

@ -181,8 +181,10 @@ func buildParams(
} }
for _, tc := range msg.ToolCalls { for _, tc := range msg.ToolCalls {
args := tc.Arguments args := tc.Arguments
if args == nil && tc.Function != nil && len(tc.Function.Arguments) > 0 { if args == nil && tc.Function != nil && tc.Function.Arguments != "" {
args = tc.Function.Arguments if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil {
args = map[string]any{}
}
} }
if args == nil { if args == nil {
args = map[string]any{} args = map[string]any{}
@ -306,19 +308,16 @@ func levelToBudget(level string) int {
func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam { func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
result := make([]anthropic.ToolUnionParam, 0, len(tools)) result := make([]anthropic.ToolUnionParam, 0, len(tools))
for _, t := range tools { for _, t := range tools {
params := t.Function.ParametersMap()
tool := anthropic.ToolParam{ tool := anthropic.ToolParam{
Name: t.Function.Name, Name: t.Function.Name,
InputSchema: anthropic.ToolInputSchemaParam{ InputSchema: anthropic.ToolInputSchemaParam{
Properties: params["properties"], Properties: t.Function.Parameters["properties"],
}, },
} }
if desc := t.Function.Description; desc != "" { if desc := t.Function.Description; desc != "" {
tool.Description = anthropic.String(desc) tool.Description = anthropic.String(desc)
} }
if req, ok := t.Function.Parameters["required"].([]any); ok {
switch req := params["required"].(type) {
case []any:
required := make([]string, 0, len(req)) required := make([]string, 0, len(req))
for _, r := range req { for _, r := range req {
if s, ok := r.(string); ok { if s, ok := r.(string); ok {
@ -326,8 +325,6 @@ func translateTools(tools []ToolDefinition) []anthropic.ToolUnionParam {
} }
} }
tool.InputSchema.Required = required tool.InputSchema.Required = required
case []string:
tool.InputSchema.Required = append([]string(nil), req...)
} }
result = append(result, anthropic.ToolUnionParam{OfTool: &tool}) result = append(result, anthropic.ToolUnionParam{OfTool: &tool})
} }

View file

@ -42,11 +42,6 @@ func BuildAgentMainSessionKey(agentID string) string {
return fmt.Sprintf("agent:%s:%s", NormalizeAgentID(agentID), DefaultMainKey) return fmt.Sprintf("agent:%s:%s", NormalizeAgentID(agentID), DefaultMainKey)
} }
// BuildSubagentSessionKey returns "subagent:<taskID>" for subagent sessions.
func BuildSubagentSessionKey(taskID string) string {
return fmt.Sprintf("subagent:%s", taskID)
}
// BuildAgentPeerSessionKey constructs a session key based on agent, channel, peer, and DM scope. // BuildAgentPeerSessionKey constructs a session key based on agent, channel, peer, and DM scope.
func BuildAgentPeerSessionKey(params SessionKeyParams) string { func BuildAgentPeerSessionKey(params SessionKeyParams) string {
agentID := NormalizeAgentID(params.AgentID) agentID := NormalizeAgentID(params.AgentID)
@ -195,3 +190,8 @@ func resolveLinkedPeerID(identityLinks map[string][]string, channel, peerID stri
} }
return "" return ""
} }
// BuildSubagentSessionKey returns "subagent:<taskID>" for subagent sessions.
func BuildSubagentSessionKey(taskID string) string {
return fmt.Sprintf("subagent:%s", taskID)
}

View file

@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv"
"time" "time"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
@ -112,7 +111,7 @@ func (c *ClawHubRegistry) Search(ctx context.Context, query string, limit int) (
q := u.Query() q := u.Query()
q.Set("q", query) q.Set("q", query)
if limit > 0 { if limit > 0 {
q.Set("limit", strconv.Itoa(limit)) q.Set("limit", fmt.Sprintf("%d", limit))
} }
u.RawQuery = q.Encode() u.RawQuery = q.Encode()

View file

@ -180,7 +180,7 @@ func (rm *RegistryManager) SearchAll(ctx context.Context, query string, limit in
close(resultsCh) close(resultsCh)
}() }()
merged := make([]SearchResult, 0, len(regs)*limit) var merged []SearchResult
var lastErr error var lastErr error
var anyRegistrySucceeded bool var anyRegistrySucceeded bool

View file

@ -39,8 +39,8 @@ func NewSearchCache(maxEntries int, ttl time.Duration) *SearchCache {
ttl = 5 * time.Minute ttl = 5 * time.Minute
} }
return &SearchCache{ return &SearchCache{
entries: make(map[string]*cacheEntry, maxEntries), entries: make(map[string]*cacheEntry),
order: make([]string, 0, maxEntries), order: make([]string, 0),
maxEntries: maxEntries, maxEntries: maxEntries,
ttl: ttl, ttl: ttl,
} }

View file

@ -14,13 +14,11 @@ import (
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
) )
var (
audioExtensions = []string{".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".wma"}
audioTypes = []string{"audio/", "application/ogg", "application/x-ogg"}
)
// IsAudioFile checks if a file is an audio file based on its filename extension and content type. // IsAudioFile checks if a file is an audio file based on its filename extension and content type.
func IsAudioFile(filename, contentType string) bool { func IsAudioFile(filename, contentType string) bool {
audioExtensions := []string{".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".wma"}
audioTypes := []string{"audio/", "application/ogg", "application/x-ogg"}
for _, ext := range audioExtensions { for _, ext := range audioExtensions {
if strings.HasSuffix(strings.ToLower(filename), ext) { if strings.HasSuffix(strings.ToLower(filename), ext) {
return true return true

View file

@ -6,9 +6,6 @@ PicoClaw 🦞
## Description ## Description
Ultra-lightweight personal AI assistant written in Go, inspired by nanobot. Ultra-lightweight personal AI assistant written in Go, inspired by nanobot.
## Version
0.1.0
## Purpose ## Purpose
- Provide intelligent AI assistance with minimal resource usage - Provide intelligent AI assistance with minimal resource usage
- Support multiple LLM providers (OpenAI, Anthropic, Zhipu, etc.) - Support multiple LLM providers (OpenAI, Anthropic, Zhipu, etc.)