Merge branch 'main' into feat/suppoer_btw_command

This commit is contained in:
lxowalle 2026-04-14 17:20:55 +08:00
commit 22f170450a
279 changed files with 17416 additions and 6613 deletions

View file

@ -23,6 +23,12 @@ jobs:
with:
go-version-file: go.mod
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
@ -30,9 +36,6 @@ jobs:
cache: pnpm
cache-dependency-path: web/frontend/pnpm-lock.yaml
- name: Setup pnpm
run: corepack enable && corepack install
# 3. Build the application bundle
- name: Build with Make
run: make build ARCH=${{ matrix.arch }} && make build-macos-app ARCH=${{ matrix.arch }}

View file

@ -47,6 +47,12 @@ jobs:
with:
go-version-file: go.mod
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
@ -54,9 +60,6 @@ jobs:
cache: pnpm
cache-dependency-path: web/frontend/pnpm-lock.yaml
- name: Setup pnpm
run: corepack enable && corepack install
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

View file

@ -65,6 +65,12 @@ jobs:
with:
go-version-file: go.mod
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.33.0
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
@ -72,9 +78,6 @@ jobs:
cache: pnpm
cache-dependency-path: web/frontend/pnpm-lock.yaml
- name: Setup pnpm
run: corepack enable && corepack install
- name: Set up QEMU
uses: docker/setup-qemu-action@v4

View file

@ -170,7 +170,7 @@ Vous pouvez aussi télécharger le binaire pour votre plateforme depuis la page
Prérequis :
- Go 1.25+
- Node.js 22+ avec Corepack activé pour les builds Web UI / launcher
- Node.js 22+ et pnpm 10.33.0+ pour les builds Web UI / launcher
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -178,8 +178,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Installer le gestionnaire de paquets frontend déclaré par le dépôt
(cd web/frontend && corepack install)
# Installer les dépendances frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Compiler le binaire principal
make build
@ -627,4 +627,3 @@ Discord : <https://discord.gg/V4sAZ9XWpN>
WeChat :
<img src="assets/wechat.png" alt="WeChat group QR code" width="512">

View file

@ -167,7 +167,7 @@ Atau, unduh binary untuk platform Anda dari halaman [GitHub Releases](https://gi
Prasyarat:
- Go 1.25+
- Node.js 22+ dengan Corepack aktif untuk build Web UI / launcher
- Node.js 22+ dan pnpm 10.33.0+ untuk build Web UI / launcher
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Instal package manager frontend yang dideklarasikan repo
(cd web/frontend && corepack install)
# Instal dependensi frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Build binary inti
make build

View file

@ -167,7 +167,7 @@ In alternativa, scarica il binario per la tua piattaforma dalla pagina delle [Gi
Prerequisiti:
- Go 1.25+
- Node.js 22+ con Corepack abilitato per le build Web UI / launcher
- Node.js 22+ e pnpm 10.33.0+ per le build Web UI / launcher
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Installa il package manager frontend dichiarato dal repository
(cd web/frontend && corepack install)
# Installa le dipendenze frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Compila il binario core
make build

View file

@ -167,7 +167,7 @@ PicoClaw はほぼすべての Linux デバイスにデプロイできます!
前提条件:
- Go 1.25+
- Web UI / launcher のビルドには Corepack を有効にした Node.js 22+
- Web UI / launcher のビルドには Node.js 22+ と pnpm 10.33.0+ が必要
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# リポジトリで宣言されたフロントエンド用パッケージマネージャーをインストール
(cd web/frontend && corepack install)
# フロントエンド依存関係をインストール
(cd web/frontend && pnpm install --frozen-lockfile)
# コアバイナリをビルド
make build

View file

@ -167,7 +167,7 @@ PicoClaw는 사실상 거의 모든 Linux 장치에 배포할 수 있습니다!
필수 사항:
- Go 1.25+
- Web UI / launcher 빌드를 위한 Corepack 활성화된 Node.js 22+
- Web UI / launcher 빌드에는 Node.js 22+와 pnpm 10.33.0+가 필요합니다
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# 저장소에 선언된 프런트엔드 패키지 매니저 설치
(cd web/frontend && corepack install)
# 프런트엔드 의존성 설치
(cd web/frontend && pnpm install --frozen-lockfile)
# 코어 바이너리 빌드
make build

View file

@ -167,7 +167,7 @@ Alternatively, download the binary for your platform from the [GitHub Releases](
Prerequisites:
- Go 1.25+
- Node.js 22+ with Corepack enabled for Web UI / launcher builds
- Node.js 22+ and pnpm 10.33.0+ for Web UI / launcher builds
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Install frontend package manager declared by the repo
(cd web/frontend && corepack install)
# Install frontend dependencies
(cd web/frontend && pnpm install --frozen-lockfile)
# Build the core binary for the current platform
make build
@ -523,7 +523,7 @@ picoclaw skills search "web scraping"
picoclaw skills install <skill-name>
```
**Configure ClawHub token** (optional, for higher rate limits):
**Configure skill registries**:
Add to your `config.json`:
```json
@ -533,6 +533,11 @@ Add to your `config.json`:
"registries": {
"clawhub": {
"auth_token": "your-clawhub-token"
},
"github": {
"base_url": "https://github.com",
"auth_token": "your-github-token",
"proxy": ""
}
}
}
@ -540,6 +545,8 @@ Add to your `config.json`:
}
```
`tools.skills.github.*` is deprecated. Use `tools.skills.registries.github.*` instead.
For more details, see [Tools Configuration - Skills](docs/tools_configuration.md#skills-tool).
## 🔗 MCP (Model Context Protocol)

View file

@ -168,15 +168,15 @@ Muat turun binari untuk platform anda dari halaman [GitHub Releases](https://git
Prasyarat:
- Go 1.25+
- Node.js 22+ dengan Corepack diaktifkan untuk binaan Web UI / launcher
- Node.js 22+ dan pnpm 10.33.0+ untuk binaan Web UI / launcher
```bash
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Pasang pengurus pakej frontend yang diisytiharkan oleh repositori
(cd web/frontend && corepack install)
# Pasang dependensi frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Bina binari teras
make build

View file

@ -167,7 +167,7 @@ Alternativamente, baixe o binário para sua plataforma na página de [GitHub Rel
Pré-requisitos:
- Go 1.25+
- Node.js 22+ com Corepack habilitado para builds do Web UI / launcher
- Node.js 22+ e pnpm 10.33.0+ para builds do Web UI / launcher
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Instalar o gerenciador de pacotes de frontend declarado pelo repositório
(cd web/frontend && corepack install)
# Instalar dependências do frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Compilar o binário principal
make build

View file

@ -167,7 +167,7 @@ Ngoài ra, tải binary cho nền tảng của bạn từ trang [GitHub Releases
Yêu cầu:
- Go 1.25+
- Node.js 22+ với Corepack được bật cho các bản build Web UI / launcher
- Node.js 22+ và pnpm 10.33.0+ cho các bản build Web UI / launcher
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Cài đặt trình quản lý gói frontend được khai báo bởi repo
(cd web/frontend && corepack install)
# Cài đặt dependencies frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Build binary lõi
make build

View file

@ -167,7 +167,7 @@ PicoClaw 几乎可以部署在任何 Linux 设备上!
前置要求:
- Go 1.25+
- Node.js 22+,并启用 Corepack(用于 Web UI / launcher 构建)
- Node.js 22+ 和 pnpm 10.33.0+(用于 Web UI / launcher 构建)
```bash
git clone https://github.com/sipeed/picoclaw.git
@ -175,8 +175,8 @@ git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# 安装仓库声明的前端包管理器
(cd web/frontend && corepack install)
# 安装前端依赖
(cd web/frontend && pnpm install --frozen-lockfile)
# 构建核心二进制文件
make build
@ -515,7 +515,7 @@ picoclaw skills search "web scraping"
picoclaw skills install <skill-name>
```
**配置 ClawHub token**(可选,用于提高速率限制)
**配置 Skills 仓库源**
`config.json` 中添加:
```json
@ -525,6 +525,11 @@ picoclaw skills install <skill-name>
"registries": {
"clawhub": {
"auth_token": "your-clawhub-token"
},
"github": {
"base_url": "https://github.com",
"auth_token": "your-github-token",
"proxy": ""
}
}
}
@ -532,6 +537,8 @@ picoclaw skills install <skill-name>
}
```
`tools.skills.github.*` 已废弃,请改用 `tools.skills.registries.github.*`
更多详情请参阅 [工具配置 - Skills](docs/zh/tools_configuration.md#skills-tool)。
## 🔗 MCP (Model Context Protocol)
@ -624,5 +631,3 @@ Discord: <https://discord.gg/V4sAZ9XWpN>
WeChat:
<img src="assets/wechat.png" alt="WeChat group QR code" width="512">

View file

@ -19,6 +19,7 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
)
const (
@ -155,11 +156,31 @@ func defaultWeComQRFlowOptions(timeout time.Duration) wecomQRFlowOptions {
}
func applyWeComAuthResult(cfg *config.Config, botInfo wecomQRBotInfo) {
cfg.Channels.WeCom.Enabled = true
cfg.Channels.WeCom.BotID = botInfo.BotID
cfg.Channels.WeCom.SetSecret(botInfo.Secret)
if strings.TrimSpace(cfg.Channels.WeCom.WebSocketURL) == "" {
cfg.Channels.WeCom.WebSocketURL = wecomDefaultWebSocketURL
bc := cfg.Channels.GetByType(config.ChannelWeCom)
if bc == nil {
bc = &config.Channel{Type: config.ChannelWeCom}
cfg.Channels["wecom"] = bc
}
bc.Enabled = true
decoded, err := bc.GetDecoded()
if err != nil {
logger.ErrorCF("wecom", "failed to decode WeCom settings", map[string]any{
"error": err.Error(),
})
return
}
wecomCfg, ok := decoded.(*config.WeComSettings)
if !ok {
logger.ErrorCF("wecom", "unexpected WeCom settings type", map[string]any{
"got": fmt.Sprintf("%T", decoded),
})
return
}
wecomCfg.BotID = botInfo.BotID
wecomCfg.Secret = *config.NewSecureString(botInfo.Secret)
if strings.TrimSpace(wecomCfg.WebSocketURL) == "" {
wecomCfg.WebSocketURL = wecomDefaultWebSocketURL
}
}

View file

@ -112,17 +112,23 @@ func TestPollWeComQRCodeResult(t *testing.T) {
func TestApplyWeComAuthResult(t *testing.T) {
cfg := config.DefaultConfig()
cfg.Channels.WeCom.WebSocketURL = ""
require.NoError(t, config.InitChannelList(cfg.Channels))
wecom := cfg.Channels["wecom"]
t.Logf("wecom: %+v", wecom)
decoded, err := wecom.GetDecoded()
require.NoError(t, err)
weCfg := decoded.(*config.WeComSettings)
weCfg.WebSocketURL = ""
applyWeComAuthResult(cfg, wecomQRBotInfo{
BotID: "bot-1",
Secret: "secret-1",
})
assert.True(t, cfg.Channels.WeCom.Enabled)
assert.Equal(t, "bot-1", cfg.Channels.WeCom.BotID)
assert.Equal(t, "secret-1", cfg.Channels.WeCom.Secret.String())
assert.Equal(t, wecomDefaultWebSocketURL, cfg.Channels.WeCom.WebSocketURL)
assert.True(t, wecom.Enabled)
assert.Equal(t, "bot-1", weCfg.BotID)
assert.Equal(t, "secret-1", weCfg.Secret.String())
assert.Equal(t, wecomDefaultWebSocketURL, weCfg.WebSocketURL)
}
func TestAuthWeComCmdWithScanner(t *testing.T) {
@ -149,9 +155,13 @@ func TestAuthWeComCmdWithScanner(t *testing.T) {
cfg, err := config.LoadConfig(internal.GetConfigPath())
require.NoError(t, err)
assert.True(t, cfg.Channels.WeCom.Enabled)
assert.Equal(t, "bot-1", cfg.Channels.WeCom.BotID)
assert.Equal(t, "secret-1", cfg.Channels.WeCom.Secret.String())
assert.Equal(t, wecomDefaultWebSocketURL, cfg.Channels.WeCom.WebSocketURL)
wecom := cfg.Channels["wecom"]
decoded, err := wecom.GetDecoded()
require.NoError(t, err)
weCfg := decoded.(*config.WeComSettings)
assert.True(t, wecom.Enabled)
assert.Equal(t, "bot-1", weCfg.BotID)
assert.Equal(t, "secret-1", weCfg.Secret.String())
assert.Equal(t, wecomDefaultWebSocketURL, weCfg.WebSocketURL)
assert.Contains(t, output.String(), "WeCom connected.")
}

View file

@ -95,14 +95,24 @@ func saveWeixinConfig(token, baseURL, proxy string) error {
return fmt.Errorf("failed to load config: %w", err)
}
cfg.Channels.Weixin.Enabled = true
cfg.Channels.Weixin.SetToken(token)
const defaultBase = "https://ilinkai.weixin.qq.com/"
if baseURL != "" && baseURL != defaultBase {
cfg.Channels.Weixin.BaseURL = baseURL
bc := cfg.Channels.GetByType(config.ChannelWeixin)
if bc == nil {
bc = &config.Channel{Type: config.ChannelWeixin}
cfg.Channels[config.ChannelWeixin] = bc
}
if proxy != "" {
cfg.Channels.Weixin.Proxy = proxy
bc.Enabled = true
if decoded, err := bc.GetDecoded(); err == nil && decoded != nil {
if weixinCfg, ok := decoded.(*config.WeixinSettings); ok {
weixinCfg.Token = *config.NewSecureString(token)
const defaultBase = "https://ilinkai.weixin.qq.com/"
if baseURL != "" && baseURL != defaultBase {
weixinCfg.BaseURL = baseURL
}
if proxy != "" {
weixinCfg.Proxy = proxy
}
}
}
return config.SaveConfig(cfgPath, cfg)

View file

@ -172,6 +172,9 @@ func copyEmbeddedToTarget(targetDir string) error {
if err != nil {
return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err)
}
if new_path == "AGENTS.md" || new_path == "IDENTITY.md" {
return nil
}
// Build target file path
targetPath := filepath.Join(targetDir, new_path)

View file

@ -12,7 +12,6 @@ import (
type deps struct {
workspace string
installer *skills.SkillInstaller
skillsLoader *skills.SkillsLoader
}
@ -29,15 +28,6 @@ func NewSkillsCommand() *cobra.Command {
}
d.workspace = cfg.WorkspacePath()
installer, err := skills.NewSkillInstaller(
d.workspace,
cfg.Tools.Skills.Github.Token.String(),
cfg.Tools.Skills.Github.Proxy,
)
if err != nil {
return fmt.Errorf("error creating skills installer: %w", err)
}
d.installer = installer
// get global config directory and builtin skills directory
globalDir := filepath.Dir(internal.GetConfigPath())
@ -52,13 +42,6 @@ func NewSkillsCommand() *cobra.Command {
},
}
installerFn := func() (*skills.SkillInstaller, error) {
if d.installer == nil {
return nil, fmt.Errorf("skills installer is not initialized")
}
return d.installer, nil
}
loaderFn := func() (*skills.SkillsLoader, error) {
if d.skillsLoader == nil {
return nil, fmt.Errorf("skills loader is not initialized")
@ -75,10 +58,10 @@ func NewSkillsCommand() *cobra.Command {
cmd.AddCommand(
newListCommand(loaderFn),
newInstallCommand(installerFn),
newInstallCommand(),
newInstallBuiltinCommand(workspaceFn),
newListBuiltinCommand(),
newRemoveCommand(installerFn),
newRemoveCommand(),
newSearchCommand(),
newShowCommand(loaderFn),
)

View file

@ -2,6 +2,7 @@ package skills
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
@ -11,12 +12,23 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/fileutil"
"github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/utils"
)
const skillsSearchMaxResults = 20
type installedSkillOriginMeta struct {
Version int `json:"version"`
OriginKind string `json:"origin_kind,omitempty"`
Registry string `json:"registry,omitempty"`
Slug string `json:"slug,omitempty"`
RegistryURL string `json:"registry_url,omitempty"`
InstalledVersion string `json:"installed_version,omitempty"`
InstalledAt int64 `json:"installed_at"`
}
func skillsListCmd(loader *skills.SkillsLoader) {
allSkills := loader.ListSkills()
@ -35,61 +47,32 @@ func skillsListCmd(loader *skills.SkillsLoader) {
}
}
func skillsInstallCmd(installer *skills.SkillInstaller, repo string) error {
fmt.Printf("Installing skill from %s...\n", repo)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := installer.InstallFromGitHub(ctx, repo); err != nil {
return fmt.Errorf("failed to install skill: %w", err)
}
fmt.Printf("\u2713 Skill '%s' installed successfully!\n", filepath.Base(repo))
return nil
}
// skillsInstallFromRegistry installs a skill from a named registry (e.g. clawhub).
func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) error {
func skillsInstallFromRegistry(cfg *config.Config, registryName, target string) error {
err := utils.ValidateSkillIdentifier(registryName)
if err != nil {
return fmt.Errorf("✗ invalid registry name: %w", err)
}
err = utils.ValidateSkillIdentifier(slug)
if err != nil {
return fmt.Errorf("✗ invalid slug: %w", err)
}
fmt.Printf("Installing skill '%s' from %s registry...\n", slug, registryName)
clawHubConfig := cfg.Tools.Skills.Registries.ClawHub
registryMgr := skills.NewRegistryManagerFromConfig(skills.RegistryConfig{
MaxConcurrentSearches: cfg.Tools.Skills.MaxConcurrentSearches,
ClawHub: skills.ClawHubConfig{
Enabled: clawHubConfig.Enabled,
BaseURL: clawHubConfig.BaseURL,
AuthToken: clawHubConfig.AuthToken.String(),
SearchPath: clawHubConfig.SearchPath,
SkillsPath: clawHubConfig.SkillsPath,
DownloadPath: clawHubConfig.DownloadPath,
Timeout: clawHubConfig.Timeout,
MaxZipSize: clawHubConfig.MaxZipSize,
MaxResponseSize: clawHubConfig.MaxResponseSize,
},
})
registryMgr := skills.NewRegistryManagerFromToolsConfig(cfg.Tools.Skills)
registry := registryMgr.GetRegistry(registryName)
if registry == nil {
return fmt.Errorf("✗ registry '%s' not found or not enabled. check your config.json.", registryName)
}
dirName, err := registry.ResolveInstallDirName(target)
if err != nil {
return fmt.Errorf("✗ invalid install target %q: %w", target, err)
}
fmt.Printf("Installing skill '%s' from %s registry...\n", target, registryName)
workspace := cfg.WorkspacePath()
targetDir := filepath.Join(workspace, "skills", slug)
targetDir := filepath.Join(workspace, "skills", dirName)
if _, err = os.Stat(targetDir); err == nil {
return fmt.Errorf("\u2717 skill '%s' already installed at %s", slug, targetDir)
return fmt.Errorf("\u2717 skill '%s' already installed at %s", dirName, targetDir)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
@ -99,7 +82,7 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) er
return fmt.Errorf("\u2717 failed to create skills directory: %v", err)
}
result, err := registry.DownloadAndInstall(ctx, slug, "", targetDir)
result, err := registry.DownloadAndInstall(ctx, target, "", targetDir)
if err != nil {
rmErr := os.RemoveAll(targetDir)
if rmErr != nil {
@ -114,14 +97,34 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) er
fmt.Printf("\u2717 Failed to remove partial install: %v\n", rmErr)
}
return fmt.Errorf("\u2717 Skill '%s' is flagged as malicious and cannot be installed.\n", slug)
return fmt.Errorf("\u2717 Skill '%s' is flagged as malicious and cannot be installed.\n", target)
}
if result.IsSuspicious {
fmt.Printf("\u26a0\ufe0f Warning: skill '%s' is flagged as suspicious.\n", slug)
fmt.Printf("\u26a0\ufe0f Warning: skill '%s' is flagged as suspicious.\n", target)
}
fmt.Printf("\u2713 Skill '%s' v%s installed successfully!\n", slug, result.Version)
if !workspaceHasValidSkillDirectory(workspace, dirName) {
_ = os.RemoveAll(targetDir)
return fmt.Errorf("✗ failed to install skill: registry archive for %q is not a valid skill", target)
}
normalizedSlug, registryURL := skills.BuildInstallMetadataForRegistryInstance(registry, target, result.Version)
installedAt := time.Now().UnixMilli()
if err := writeInstalledSkillOriginMeta(targetDir, installedSkillOriginMeta{
Version: 1,
OriginKind: "third_party",
Registry: registry.Name(),
Slug: normalizedSlug,
RegistryURL: registryURL,
InstalledVersion: result.Version,
InstalledAt: installedAt,
}); err != nil {
_ = os.RemoveAll(targetDir)
return fmt.Errorf("✗ failed to persist skill metadata: %w", err)
}
fmt.Printf("\u2713 Skill '%s' v%s installed successfully!\n", dirName, result.Version)
if result.Summary != "" {
fmt.Printf(" %s\n", result.Summary)
}
@ -129,15 +132,51 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) er
return nil
}
func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) {
fmt.Printf("Removing skill '%s'...\n", skillName)
if err := installer.Uninstall(skillName); err != nil {
fmt.Printf("✗ Failed to remove skill: %v\n", err)
os.Exit(1)
func writeInstalledSkillOriginMeta(targetDir string, meta installedSkillOriginMeta) error {
data, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return err
}
return fileutil.WriteFileAtomic(filepath.Join(targetDir, ".skill-origin.json"), data, 0o600)
}
fmt.Printf("✓ Skill '%s' removed successfully!\n", skillName)
func workspaceHasValidSkillDirectory(workspace, directory string) bool {
loader := skills.NewSkillsLoader(workspace, "", "")
for _, skill := range loader.ListSkills() {
if skill.Source != "workspace" {
continue
}
if filepath.Base(filepath.Dir(skill.Path)) == directory {
return true
}
}
return false
}
func skillsRemoveFromWorkspace(workspace string, toolsConfig config.SkillsToolsConfig, skillName string) error {
name := strings.TrimSpace(skillName)
name = strings.Trim(name, "/")
if name == "" {
return fmt.Errorf("skill name is required")
}
if strings.Contains(name, "/") {
dirName, err := skills.GitHubInstallDirNameFromToolsConfig(toolsConfig, name)
if err != nil || dirName == "" {
return fmt.Errorf("invalid skill name %q", skillName)
}
name = dirName
}
if name == "." || name == ".." {
return fmt.Errorf("invalid skill name %q", skillName)
}
skillDir := filepath.Join(workspace, "skills", name)
if _, err := os.Stat(skillDir); os.IsNotExist(err) {
return fmt.Errorf("skill '%s' not found", name)
}
if err := os.RemoveAll(skillDir); err != nil {
return fmt.Errorf("failed to remove skill '%s': %w", name, err)
}
return nil
}
func skillsInstallBuiltinCmd(workspace string) {
@ -237,21 +276,7 @@ func skillsSearchCmd(query string) {
return
}
clawHubConfig := cfg.Tools.Skills.Registries.ClawHub
registryMgr := skills.NewRegistryManagerFromConfig(skills.RegistryConfig{
MaxConcurrentSearches: cfg.Tools.Skills.MaxConcurrentSearches,
ClawHub: skills.ClawHubConfig{
Enabled: clawHubConfig.Enabled,
BaseURL: clawHubConfig.BaseURL,
AuthToken: clawHubConfig.AuthToken.String(),
SearchPath: clawHubConfig.SearchPath,
SkillsPath: clawHubConfig.SkillsPath,
DownloadPath: clawHubConfig.DownloadPath,
Timeout: clawHubConfig.Timeout,
MaxZipSize: clawHubConfig.MaxZipSize,
MaxResponseSize: clawHubConfig.MaxResponseSize,
},
})
registryMgr := skills.NewRegistryManagerFromToolsConfig(cfg.Tools.Skills)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

View file

@ -0,0 +1,191 @@
package skills
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/sipeed/picoclaw/pkg/config"
)
func TestSkillsInstallFromRegistryWritesOriginMetadata(t *testing.T) {
workspace := t.TempDir()
cfg := config.DefaultConfig()
cfg.Agents.Defaults.Workspace = workspace
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v3/repos/foo/bar":
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"default_branch": "master"}))
case "/api/v3/repos/foo/bar/contents/.agents/skills/pr-review":
assert.Equal(t, "ref=master", r.URL.RawQuery)
require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
"type": "file",
"name": "SKILL.md",
"download_url": server.URL + "/raw/foo/bar/master/.agents/skills/pr-review/SKILL.md",
}}))
case "/raw/foo/bar/master/.agents/skills/pr-review/SKILL.md":
_, _ = w.Write([]byte("---\nname: pr-review\ndescription: PR review skill\n---\n# PR Review\n"))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
githubRegistry, ok := cfg.Tools.Skills.Registries.Get("github")
require.True(t, ok)
githubRegistry.BaseURL = server.URL
cfg.Tools.Skills.Registries.Set("github", githubRegistry)
target := server.URL + "/foo/bar/tree/master/.agents/skills/pr-review"
require.NoError(t, skillsInstallFromRegistry(cfg, "github", target))
metaPath := filepath.Join(workspace, "skills", "pr-review", ".skill-origin.json")
data, err := os.ReadFile(metaPath)
require.NoError(t, err)
var meta installedSkillOriginMeta
require.NoError(t, json.Unmarshal(data, &meta))
assert.Equal(t, "third_party", meta.OriginKind)
assert.Equal(t, "github", meta.Registry)
assert.Equal(t, "foo/bar/.agents/skills/pr-review", meta.Slug)
assert.Equal(t, server.URL+"/foo/bar/tree/master/.agents/skills/pr-review", meta.RegistryURL)
assert.Equal(t, "master", meta.InstalledVersion)
assert.NotZero(t, meta.InstalledAt)
}
func TestSkillsInstallFromRegistryRejectsInvalidSkillArchive(t *testing.T) {
workspace := t.TempDir()
cfg := config.DefaultConfig()
cfg.Agents.Defaults.Workspace = workspace
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v3/repos/foo/bar":
require.NoError(t, json.NewEncoder(w).Encode(map[string]any{"default_branch": "master"}))
case "/api/v3/repos/foo/bar/contents/.agents/skills/pr-review":
require.NoError(t, json.NewEncoder(w).Encode([]map[string]any{{
"type": "file",
"name": "SKILL.md",
"download_url": server.URL + "/raw/foo/bar/master/.agents/skills/pr-review/SKILL.md",
}}))
case "/raw/foo/bar/master/.agents/skills/pr-review/SKILL.md":
_, _ = w.Write([]byte("---\nname: bad_skill\ndescription: Invalid skill name\n---\n# Invalid\n"))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
githubRegistry, ok := cfg.Tools.Skills.Registries.Get("github")
require.True(t, ok)
githubRegistry.BaseURL = server.URL
cfg.Tools.Skills.Registries.Set("github", githubRegistry)
target := server.URL + "/foo/bar/tree/master/.agents/skills/pr-review"
err := skillsInstallFromRegistry(cfg, "github", target)
require.Error(t, err)
assert.Contains(t, err.Error(), "is not a valid skill")
_, statErr := os.Stat(filepath.Join(workspace, "skills", "pr-review"))
assert.True(t, os.IsNotExist(statErr))
}
func TestSkillsRemoveFromWorkspaceRejectsDotTarget(t *testing.T) {
workspace := t.TempDir()
skillsDir := filepath.Join(workspace, "skills")
require.NoError(t, os.MkdirAll(skillsDir, 0o755))
require.NoError(t, os.WriteFile(filepath.Join(skillsDir, "keep.txt"), []byte("keep"), 0o644))
err := skillsRemoveFromWorkspace(workspace, config.DefaultConfig().Tools.Skills, ".")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid skill name")
_, statErr := os.Stat(skillsDir)
assert.NoError(t, statErr)
_, fileErr := os.Stat(filepath.Join(skillsDir, "keep.txt"))
assert.NoError(t, fileErr)
}
func TestSkillsRemoveFromWorkspaceUsesLastPathSegment(t *testing.T) {
workspace := t.TempDir()
targetDir := filepath.Join(workspace, "skills", "pr-review")
require.NoError(t, os.MkdirAll(targetDir, 0o755))
err := skillsRemoveFromWorkspace(
workspace,
config.DefaultConfig().Tools.Skills,
"https://github.com/foo/bar/tree/main/.agents/skills/pr-review",
)
require.NoError(t, err)
_, statErr := os.Stat(targetDir)
assert.True(t, os.IsNotExist(statErr))
}
func TestSkillsRemoveFromWorkspaceSupportsRepoRootGitHubBlobURL(t *testing.T) {
workspace := t.TempDir()
targetDir := filepath.Join(workspace, "skills", "bar")
require.NoError(t, os.MkdirAll(targetDir, 0o755))
err := skillsRemoveFromWorkspace(
workspace,
config.DefaultConfig().Tools.Skills,
"https://github.com/foo/bar/blob/feature/skills-registry/SKILL.md",
)
require.NoError(t, err)
_, statErr := os.Stat(targetDir)
assert.True(t, os.IsNotExist(statErr))
}
func TestSkillsRemoveFromWorkspaceSupportsGitHubEnterpriseURL(t *testing.T) {
workspace := t.TempDir()
targetDir := filepath.Join(workspace, "skills", "pr-review")
require.NoError(t, os.MkdirAll(targetDir, 0o755))
cfg := config.DefaultConfig()
githubRegistry, ok := cfg.Tools.Skills.Registries.Get("github")
require.True(t, ok)
githubRegistry.BaseURL = "https://ghe.example.com/git"
cfg.Tools.Skills.Registries.Set("github", githubRegistry)
err := skillsRemoveFromWorkspace(
workspace,
cfg.Tools.Skills,
"https://ghe.example.com/git/foo/bar/tree/main/.agents/skills/pr-review",
)
require.NoError(t, err)
_, statErr := os.Stat(targetDir)
assert.True(t, os.IsNotExist(statErr))
}
func TestSkillsRemoveFromWorkspaceDoesNotRequireEnabledGitHubRegistry(t *testing.T) {
workspace := t.TempDir()
targetDir := filepath.Join(workspace, "skills", "pr-review")
require.NoError(t, os.MkdirAll(targetDir, 0o755))
cfg := config.DefaultConfig()
githubRegistry, ok := cfg.Tools.Skills.Registries.Get("github")
require.True(t, ok)
githubRegistry.Enabled = false
cfg.Tools.Skills.Registries.Set("github", githubRegistry)
err := skillsRemoveFromWorkspace(
workspace,
cfg.Tools.Skills,
"https://github.com/foo/bar/tree/main/.agents/skills/pr-review",
)
require.NoError(t, err)
_, statErr := os.Stat(targetDir)
assert.True(t, os.IsNotExist(statErr))
}

View file

@ -6,15 +6,14 @@ import (
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/skills"
)
func newInstallCommand(installerFn func() (*skills.SkillInstaller, error)) *cobra.Command {
func newInstallCommand() *cobra.Command {
var registry string
cmd := &cobra.Command{
Use: "install",
Short: "Install skill from GitHub",
Short: "Install skill from GitHub or a registry",
Example: `
picoclaw skills install sipeed/picoclaw-skills/weather
picoclaw skills install --registry clawhub github
@ -34,21 +33,15 @@ picoclaw skills install --registry clawhub github
return nil
},
RunE: func(_ *cobra.Command, args []string) error {
installer, err := installerFn()
cfg, err := internal.LoadConfig()
if err != nil {
return err
}
if registry != "" {
cfg, err := internal.LoadConfig()
if err != nil {
return err
}
return skillsInstallFromRegistry(cfg, registry, args[0])
}
return skillsInstallCmd(installer, args[0])
return skillsInstallFromRegistry(cfg, "github", args[0])
},
}

View file

@ -8,12 +8,12 @@ import (
)
func TestNewInstallSubcommand(t *testing.T) {
cmd := newInstallCommand(nil)
cmd := newInstallCommand()
require.NotNil(t, cmd)
assert.Equal(t, "install", cmd.Use)
assert.Equal(t, "Install skill from GitHub", cmd.Short)
assert.Equal(t, "Install skill from GitHub or a registry", cmd.Short)
assert.Nil(t, cmd.Run)
assert.NotNil(t, cmd.RunE)
@ -79,7 +79,7 @@ func TestInstallCommandArgs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := newInstallCommand(nil)
cmd := newInstallCommand()
if tt.registry != "" {
require.NoError(t, cmd.Flags().Set("registry", tt.registry))

View file

@ -3,10 +3,10 @@ package skills
import (
"github.com/spf13/cobra"
"github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
)
func newRemoveCommand(installerFn func() (*skills.SkillInstaller, error)) *cobra.Command {
func newRemoveCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "remove",
Aliases: []string{"rm", "uninstall"},
@ -14,12 +14,11 @@ func newRemoveCommand(installerFn func() (*skills.SkillInstaller, error)) *cobra
Args: cobra.ExactArgs(1),
Example: `picoclaw skills remove weather`,
RunE: func(_ *cobra.Command, args []string) error {
installer, err := installerFn()
cfg, err := internal.LoadConfig()
if err != nil {
return err
}
skillsRemoveCmd(installer, args[0])
return nil
return skillsRemoveFromWorkspace(cfg.WorkspacePath(), cfg.Tools.Skills, args[0])
},
}

View file

@ -8,7 +8,7 @@ import (
)
func TestNewRemoveSubcommand(t *testing.T) {
cmd := newRemoveCommand(nil)
cmd := newRemoveCommand()
require.NotNil(t, cmd)

View file

@ -382,9 +382,16 @@
"timeout": 0,
"max_zip_size": 0,
"max_response_size": 0
},
"github": {
"enabled": true,
"base_url": "https://github.com",
"auth_token": "",
"proxy": "http://127.0.0.1:7891"
}
},
"github": {
"base_url": "https://github.com",
"proxy": "http://127.0.0.1:7891",
"token": ""
},

View file

@ -8,9 +8,10 @@ DingTalk est la plateforme de communication d'entreprise d'Alibaba, très popula
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ DingTalkはアリババの企業向けコミュニケーションプラットフ
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ DingTalk is Alibaba's enterprise communication platform, widely used in Chinese
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ DingTalk é a plataforma de comunicação empresarial da Alibaba, amplamente uti
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ DingTalk là nền tảng giao tiếp doanh nghiệp của Alibaba, được s
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ Discord est une application gratuite de chat vocal, vidéo et textuel conçue po
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {

View file

@ -8,9 +8,10 @@ Discord はコミュニティ向けに設計された無料の音声・ビデオ
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {

View file

@ -8,9 +8,10 @@ Discord is a free voice, video, and text chat application designed for communiti
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {

View file

@ -8,9 +8,10 @@ Discord é um aplicativo gratuito de chat de voz, vídeo e texto projetado para
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {

View file

@ -8,9 +8,10 @@ Discord là ứng dụng chat thoại, video và văn bản miễn phí được
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {

View file

@ -8,9 +8,10 @@ Discord 是一个专为社区设计的免费语音、视频和文本聊天应用
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"group_trigger": {

View file

@ -8,9 +8,10 @@ Feishu (nom international : Lark) est une plateforme de collaboration d'entrepri
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",

View file

@ -8,9 +8,10 @@
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",

View file

@ -8,9 +8,10 @@ Feishu (international name: Lark) is an enterprise collaboration platform by Byt
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",

View file

@ -8,9 +8,10 @@ Feishu (nome internacional: Lark) é uma plataforma de colaboração empresarial
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",

View file

@ -8,9 +8,10 @@ Feishu (tên quốc tế: Lark) là nền tảng cộng tác doanh nghiệp củ
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",

View file

@ -8,9 +8,10 @@
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",

View file

@ -8,9 +8,10 @@ PicoClaw prend en charge LINE via l'API LINE Messaging avec des callbacks webhoo
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",

View file

@ -8,9 +8,10 @@ PicoClaw は LINE Messaging API と Webhook コールバックを通じて LINE
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",

View file

@ -8,9 +8,10 @@ PicoClaw supports LINE through the LINE Messaging API with webhook callbacks.
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",

View file

@ -8,9 +8,10 @@ O PicoClaw suporta o LINE por meio da LINE Messaging API com callbacks de webhoo
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",

View file

@ -8,9 +8,10 @@ PicoClaw hỗ trợ LINE thông qua LINE Messaging API kết hợp với webhook
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",

View file

@ -8,9 +8,10 @@ PicoClaw 通过 LINE Messaging API 配合 Webhook 回调功能实现对 LINE 的
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",

View file

@ -8,9 +8,10 @@ MaixCam est un canal dédié à la connexion aux caméras AI Sipeed MaixCAM et M
```json
{
"channels": {
"channel_list": {
"maixcam": {
"enabled": true,
"type": "maixcam",
"host": "0.0.0.0",
"port": 18790,
"allow_from": []

View file

@ -8,9 +8,10 @@ MaixCam は、Sipeed MaixCAM および MaixCAM2 AI カメラデバイスへの
```json
{
"channels": {
"channel_list": {
"maixcam": {
"enabled": true,
"type": "maixcam",
"host": "0.0.0.0",
"port": 18790,
"allow_from": []

View file

@ -8,9 +8,10 @@ MaixCam is a dedicated channel for connecting to Sipeed MaixCAM and MaixCAM2 AI
```json
{
"channels": {
"channel_list": {
"maixcam": {
"enabled": true,
"type": "maixcam",
"host": "0.0.0.0",
"port": 18790,
"allow_from": []

View file

@ -8,9 +8,10 @@ MaixCam é um canal dedicado para conectar dispositivos de câmera AI Sipeed Mai
```json
{
"channels": {
"channel_list": {
"maixcam": {
"enabled": true,
"type": "maixcam",
"host": "0.0.0.0",
"port": 18790,
"allow_from": []

View file

@ -8,9 +8,10 @@ MaixCam là kênh chuyên dụng để kết nối với các thiết bị camer
```json
{
"channels": {
"channel_list": {
"maixcam": {
"enabled": true,
"type": "maixcam",
"host": "0.0.0.0",
"port": 18790,
"allow_from": []

View file

@ -8,9 +8,10 @@ MaixCam 是专用于连接矽速科技 MaixCAM 与 MaixCAM2 AI 摄像设备的
```json
{
"channels": {
"channel_list": {
"maixcam": {
"enabled": true,
"type": "maixcam",
"host": "0.0.0.0",
"port": 18790,
"allow_from": []

View file

@ -8,9 +8,10 @@ Ajoutez ceci à `config.json` :
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",

View file

@ -8,9 +8,10 @@
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",

View file

@ -8,9 +8,10 @@ Add this to `config.json`:
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",

View file

@ -8,9 +8,10 @@ Adicione isto ao `config.json`:
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",

View file

@ -8,9 +8,10 @@ Thêm vào `config.json`:
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",

View file

@ -8,9 +8,10 @@
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",

View file

@ -8,9 +8,10 @@ OneBot est un standard de protocole ouvert pour les bots QQ, fournissant une int
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []

View file

@ -8,9 +8,10 @@ OneBot は QQ ボット向けのオープンプロトコル標準で、複数の
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []

View file

@ -8,9 +8,10 @@ OneBot is an open protocol standard for QQ bots, providing a unified interface f
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []

View file

@ -8,9 +8,10 @@ OneBot é um padrão de protocolo aberto para bots QQ, fornecendo uma interface
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []

View file

@ -8,9 +8,10 @@ OneBot là tiêu chuẩn giao thức mở dành cho bot QQ, cung cấp giao di
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []

View file

@ -8,9 +8,10 @@ OneBot 是一个面向 QQ 机器人的开放协议标准,为多种 QQ 机器
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://localhost:8080",
"access_token": "",
"allow_from": []

View file

@ -8,9 +8,10 @@ PicoClaw prend en charge QQ via l'API Bot officielle de la plateforme ouverte QQ
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ PicoClaw は QQ オープンプラットフォームの公式 Bot API を通じ
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ PicoClaw provides QQ support via the official Bot API from the QQ Open Platform.
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ O PicoClaw oferece suporte ao QQ via API Bot oficial da Plataforma Aberta QQ.
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ PicoClaw hỗ trợ QQ thông qua API Bot chính thức của Nền tảng Mở
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []

View file

@ -8,9 +8,10 @@ PicoClaw 通过 QQ 开放平台的官方机器人 API 提供对 QQ 的支持。
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": [],

View file

@ -8,9 +8,10 @@ Slack est l'une des principales plateformes de messagerie instantanée pour les
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []

View file

@ -8,9 +8,10 @@ Slack は世界をリードする企業向けインスタントメッセージ
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []

View file

@ -8,9 +8,10 @@ Slack is a leading enterprise instant messaging platform. PicoClaw uses Slack's
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []

View file

@ -8,9 +8,10 @@ O Slack é uma das principais plataformas de mensagens instantâneas para empres
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []

View file

@ -8,9 +8,10 @@ Slack là nền tảng nhắn tin tức thì hàng đầu dành cho doanh nghi
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []

View file

@ -8,9 +8,10 @@ Slack 是全球领先的企业级即时通讯平台。PicoClaw 采用 Slack 的
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-...",
"app_token": "xapp-...",
"allow_from": []

View file

@ -8,9 +8,10 @@ Le canal Telegram utilise le long polling via l'API Bot Telegram pour une commun
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": "",
@ -42,9 +43,10 @@ Vous pouvez définir `use_markdown_v2: true` pour activer les options de formata
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true

View file

@ -8,9 +8,10 @@ Telegram チャンネルは、Telegram Bot API を使用したロングポーリ
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": "",
@ -42,9 +43,10 @@ Telegram チャンネルは、Telegram Bot API を使用したロングポーリ
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true

View file

@ -8,9 +8,10 @@ The Telegram channel uses long polling via the Telegram Bot API for bot-based co
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": "",
@ -62,9 +63,10 @@ You can set `use_markdown_v2: true` to enable enhanced formatting options. This
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true

View file

@ -8,9 +8,10 @@ O canal Telegram utiliza long polling via a API de Bot do Telegram para comunica
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": "",
@ -42,9 +43,10 @@ Você pode definir `use_markdown_v2: true` para habilitar opções de formataç
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true

View file

@ -8,9 +8,10 @@ Kênh Telegram sử dụng long polling qua Telegram Bot API để giao tiếp d
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": "",
@ -42,9 +43,10 @@ Bạn có thể đặt `use_markdown_v2: true` để bật các tùy chọn đ
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true

View file

@ -8,9 +8,10 @@ Telegram Channel 通过 Telegram 机器人 API 使用长轮询实现基于机器
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"],
"proxy": "",
@ -62,9 +63,10 @@ explain how to squash the last 3 commits
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true

View file

@ -6,9 +6,10 @@ The VK channel uses Bots Long Poll API for bot-based communication with VK socia
```json
{
"channels": {
"channel_list": {
"vk": {
"enabled": true,
"type": "vk",
"token": "NOT_HERE",
"group_id": 123456789,
"allow_from": ["123456789"],
@ -120,9 +121,10 @@ VK has a maximum message length of 4000 characters. PicoClaw automatically split
```json
{
"channels": {
"channel_list": {
"vk": {
"enabled": true,
"type": "vk",
"token": "NOT_HERE",
"group_id": 123456789
}
@ -134,9 +136,10 @@ VK has a maximum message length of 4000 characters. PicoClaw automatically split
```json
{
"channels": {
"channel_list": {
"vk": {
"enabled": true,
"type": "vk",
"token": "NOT_HERE",
"group_id": 123456789,
"allow_from": ["123456789", "987654321"]
@ -149,9 +152,10 @@ VK has a maximum message length of 4000 characters. PicoClaw automatically split
```json
{
"channels": {
"channel_list": {
"vk": {
"enabled": true,
"type": "vk",
"token": "NOT_HERE",
"group_id": 123456789,
"group_trigger": {

View file

@ -56,9 +56,10 @@ Si vous disposez déjà d'un `bot_id` et d'un `secret` depuis la plateforme WeCo
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com",

View file

@ -56,9 +56,10 @@ WeCom AI Bot プラットフォームから `bot_id` と `secret` を既にお
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com",

View file

@ -56,9 +56,10 @@ If you already have a `bot_id` and `secret` from the WeCom AI Bot platform, conf
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com",

View file

@ -56,9 +56,10 @@ Se você já possui um `bot_id` e `secret` da plataforma WeCom AI Bot, configure
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com",

View file

@ -56,9 +56,10 @@ Nếu bạn đã có `bot_id` và `secret` từ nền tảng WeCom AI Bot, hãy
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com",

View file

@ -56,9 +56,10 @@ picoclaw auth wecom --timeout 10m
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com",

View file

@ -29,9 +29,10 @@ You can also manually configure the filter rules in `config.json` under the `cha
```json
{
"channels": {
"channel_list": {
"weixin": {
"enabled": true,
"type": "weixin",
"token": "YOUR_WEIXIN_TOKEN",
"allow_from": [
"user_id_1",

View file

@ -29,9 +29,10 @@ picoclaw gateway
```json
{
"channels": {
"channel_list": {
"weixin": {
"enabled": true,
"type": "weixin",
"token": "YOUR_WEIXIN_TOKEN",
"allow_from": [
"user_id_1",

View file

@ -40,9 +40,10 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, Matrix, QQ, DingTalk,
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": false
@ -102,9 +103,10 @@ You can set use_markdown_v2: true to enable enhanced formatting options. This al
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"]
}
@ -125,7 +127,7 @@ By default the bot responds to all messages in a server channel. To restrict res
```json
{
"channels": {
"channel_list": {
"discord": {
"group_trigger": { "mention_only": true }
}
@ -137,7 +139,7 @@ You can also trigger by keyword prefixes (e.g. `!bot`):
```json
{
"channels": {
"channel_list": {
"discord": {
"group_trigger": { "prefixes": ["!bot"] }
}
@ -166,9 +168,10 @@ PicoClaw can connect to WhatsApp in two ways:
```json
{
"channels": {
"channel_list": {
"whatsapp": {
"enabled": true,
"type": "whatsapp",
"use_native": true,
"session_store_path": "",
"allow_from": []
@ -200,9 +203,10 @@ Scan the printed QR code with your WeChat mobile app. On success, the token is s
(Optional) Update `allow_from` with your WeChat User ID to restrict who can message the bot:
```json
{
"channels": {
"channel_list": {
"weixin": {
"enabled": true,
"type": "weixin",
"token": "YOUR_TOKEN",
"allow_from": ["YOUR_USER_ID"]
}
@ -231,9 +235,10 @@ QQ Open Platform provides a one-click setup page for OpenClaw-compatible bots:
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
@ -273,9 +278,10 @@ If you prefer to create the bot manually:
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
@ -306,9 +312,10 @@ picoclaw gateway
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",
@ -342,9 +349,10 @@ For full options (`device_id`, `join_on_invite`, `group_trigger`, `placeholder`,
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",
@ -400,9 +408,10 @@ This command shows a QR code, waits for approval in WeCom, and writes `bot_id` +
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com",
@ -441,9 +450,10 @@ PicoClaw connects to Feishu via WebSocket/SDK mode — no public webhook URL or
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
@ -481,9 +491,10 @@ For full options, see [Feishu Channel Configuration Guide](channels/feishu/READM
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-YOUR-BOT-TOKEN",
"app_token": "xapp-YOUR-APP-TOKEN",
"allow_from": []
@ -508,9 +519,10 @@ picoclaw gateway
```json
{
"channels": {
"channel_list": {
"irc": {
"enabled": true,
"type": "irc",
"server": "irc.libera.chat:6697",
"tls": true,
"nick": "picoclaw-bot",
@ -548,9 +560,10 @@ Install and run a OneBot v11 compatible QQ bot framework. Enable its WebSocket s
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://127.0.0.1:8080",
"access_token": "",
"allow_from": []

View file

@ -20,6 +20,16 @@ PicoClaw uses a schema versioning system for `config.json` to ensure smooth upgr
- V0 configs now migrate directly to CurrentVersion (V2) instead of going through V1
- `makeBackup()` now uses date-only suffix (e.g., `config.json.20260330.bak`) and also backs up `.security.yml`
### Version 3
- **Introduction**: Enhanced type safety and improved error handling
- **Changes**:
- Added comma-ok type assertions in channel configuration decoding to prevent potential panics
- Improved error logging for Weixin channel configuration decoding
- Enhanced security configuration documentation and examples
- **Auto-migration**: V2 configs are automatically migrated to V3 on load with no user action required
- **Backup**: Before migration, the system creates a date-stamped backup (e.g., `config.json.20260413.bak`) in the same directory
- **Downgrade risk**: Once migrated to V3, the config cannot be safely loaded by older V2-only versions. To downgrade, restore from the auto-created backup file.
## How It Works
### Automatic Migration
@ -39,7 +49,7 @@ The `version` field in `config.json` indicates the schema version:
```json
{
"version": 2,
"version": 3,
"agents": {...},
...
}
@ -164,6 +174,52 @@ func TestMigrateV2ToV3(t *testing.T) {
7. **Test Thoroughly**: Test with real user config files
8. **Update Defaults**: Keep `defaults.go` in sync with the latest schema
## V2→V3 Migration Guide
### What Changed?
Version 3 introduces improved type safety and error handling:
- **Type-safe channel decoding**: All channel type assertions now use comma-ok pattern (`val, ok := v.(*Settings)`) to prevent panics if Type and Settings are mismatched
- **Enhanced error logging**: Weixin channel now logs errors on `GetDecoded()` failure for consistency with other channels
- **Documentation fixes**: Corrected stray quotes in JSON configuration examples
### Auto-Migration Behavior
When you run PicoClaw with a V2 config file:
1. **Detection**: PicoClaw reads the `version` field and detects V2
2. **Backup**: Before any changes, creates `config.json.YYYYMMDD.bak` (e.g., `config.json.20260413.bak`)
3. **Migration**: Applies V2→V3 structural changes (primarily internal type safety improvements)
4. **Save**: Writes the updated config with `"version": 3`
5. **Continue**: Starts normally with the V3 config
**No user action required** — the migration happens automatically on first load.
### Backup Location
Backups are created in the same directory as your config file:
- **Default**: `~/.picoclaw/config.json.20260413.bak`
- **Custom path**: If using `PICOCLAW_CONFIG`, backup is created next to that file
- **Security file**: `.security.yml` is also backed up as `.security.yml.YYYYMMDD.bak`
### Downgrade Risk
⚠️ **Important**: Once migrated to V3, the config **cannot** be safely loaded by older PicoClaw versions that only support V2.
**To downgrade:**
1. Stop PicoClaw
2. Restore the backup:
```bash
cp ~/.picoclaw/config.json.20260413.bak ~/.picoclaw/config.json
cp ~/.picoclaw/.security.yml.20260413.bak ~/.picoclaw/.security.yml # if it exists
```
3. Use a PicoClaw version that supports V2 configs
**Alternative**: Manually edit `config.json` and change `"version": 3` to `"version": 2`. This works because V3 changes are primarily code-level safety improvements, not structural schema changes.
## Example Migration
### Scenario: Adding a new field with default value
@ -171,7 +227,7 @@ func TestMigrateV2ToV3(t *testing.T) {
Old config (version 2):
```json
{
"version": 2,
"version": 3,
"model_list": [
{
"model_name": "gpt-5.4",

View file

@ -122,133 +122,78 @@ dammi le ultime news
- Unknown slash command (for example `/foo`) passes through to normal LLM processing.
- Registered but unsupported command on the current channel (for example `/show` on WhatsApp) returns an explicit user-facing error and stops further processing.
### Agent Bindings (Route messages to specific agents)
### Routing
Use `bindings` in `config.json` to route incoming messages to different agents by channel/account/context.
Routing is configured through `agents.dispatch.rules`.
Each rule matches against the normalized inbound context produced by channels.
Rules are evaluated from top to bottom. The first matching rule wins. If no
rule matches, PicoClaw falls back to the configured default agent.
Supported match fields:
* `channel`
* `account`
* `space`
* `chat`
* `topic`
* `sender`
* `mentioned`
Match values use the same scope vocabulary as the session system:
* `space`: `workspace:t001`, `guild:123456`
* `chat`: `direct:user123`, `group:-100123`, `channel:c123`
* `topic`: `topic:42`
* `sender`: a normalized sender identifier for the platform
Rules may optionally override the global `session.dimensions` value through
`session_dimensions`. This allows routing and session allocation to stay aligned
without reintroducing the old `bindings` or `dm_scope` formats.
Example:
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model_name": "gpt-4o-mini"
},
"list": [
{ "id": "main", "default": true, "name": "Main Assistant" },
{ "id": "support", "name": "Support Assistant" },
{ "id": "sales", "name": "Sales Assistant" }
]
},
"bindings": [
{
"agent_id": "support",
"match": {
"channel": "telegram",
"account_id": "*",
"peer": { "kind": "direct", "id": "user123" }
}
},
{
"agent_id": "sales",
"match": {
"channel": "discord",
"account_id": "my-discord-bot",
"guild_id": "987654321"
}
{ "id": "main", "default": true },
{ "id": "support" },
{ "id": "sales" }
],
"dispatch": {
"rules": [
{
"name": "vip in support group",
"agent": "sales",
"when": {
"channel": "telegram",
"chat": "group:-1001234567890",
"sender": "12345"
},
"session_dimensions": ["chat", "sender"]
},
{
"name": "telegram support group",
"agent": "support",
"when": {
"channel": "telegram",
"chat": "group:-1001234567890"
},
"session_dimensions": ["chat"]
}
]
}
]
}
```
#### `bindings` fields
| Field | Required | Description |
|-------|----------|-------------|
| `agent_id` | Yes | Target agent id in `agents.list` |
| `match.channel` | Yes | Channel name (e.g. `telegram`, `discord`) |
| `match.account_id` | No | Channel account filter. Use `"*"` for all accounts of that channel. If omitted, only default account is matched |
| `match.peer.kind` + `match.peer.id` | No | Exact peer match (e.g. direct chat / topic / group id) |
| `match.guild_id` | No | Guild/server-level match |
| `match.team_id` | No | Team/workspace-level match |
#### Matching priority
When multiple bindings exist, PicoClaw resolves in this order:
1. `peer`
2. `parent_peer` (for thread/topic parent contexts)
3. `guild_id`
4. `team_id`
5. `account_id` (non-wildcard)
6. channel wildcard (`account_id: "*"`)
7. default agent
If a binding points to a missing `agent_id`, PicoClaw falls back to the default agent.
#### How matching works (step-by-step)
1. PicoClaw first filters bindings by `match.channel` (must equal current channel).
2. It then filters by `match.account_id`:
- omitted: match only the channel's default account
- `"*"`: match all accounts on this channel
- explicit value: exact account id match (case-insensitive)
3. From the remaining candidates, it applies the priority chain above and stops at the first hit.
In other words: **channel + account form the candidate set; peer/guild/team then decide final winner**.
#### Common recipes
**1) Route one specific DM user to a specialist agent**
```json
{
"agent_id": "support",
"match": {
"channel": "telegram",
"account_id": "*",
"peer": { "kind": "direct", "id": "user123" }
},
"session": {
"dimensions": ["chat"]
}
}
```
**2) Route one Discord server (guild) to a dedicated agent**
```json
{
"agent_id": "sales",
"match": {
"channel": "discord",
"account_id": "my-discord-bot",
"guild_id": "987654321"
}
}
```
**3) Route all remaining traffic of a channel to a fallback agent**
```json
{
"agent_id": "main",
"match": {
"channel": "discord",
"account_id": "*"
}
}
```
#### Authoring guidelines (important)
- Keep exactly one clear default agent in `agents.list` (`"default": true`).
- Put specific rules (`peer`, `guild_id`, `team_id`) and broad rules (`account_id: "*"` only) together safely; priority already guarantees specific rules win.
- Avoid duplicate rules with the same specificity and match values. If duplicates exist, the first matching entry in the config array wins.
- Ensure every `agent_id` exists in `agents.list`; unknown IDs silently fall back to default.
#### Troubleshooting checklist
- **Rule not taking effect?** Check `match.channel` spelling first (must be exact).
- **Expected account-specific routing but still using default?** Verify `match.account_id` equals actual runtime account id.
- **Wildcard catches too much traffic?** Add more specific `peer/guild/team` rules for critical paths.
- **Unexpected default fallback?** Confirm `agent_id` exists and is not misspelled.
In the example above, the VIP rule must appear before the broader group rule.
Because routing is strictly ordered, more specific rules should be placed
earlier and broader fallback rules later.
### 🔒 Security Sandbox
@ -594,9 +539,10 @@ chmod 600 ~/.picoclaw/.security.yml
// api_key loaded from .security.yml
}
],
"channels": {
"channel_list": {
"telegram": {
"enabled": true"
"enabled": true,
"type": "telegram",
// token loaded from .security.yml
}
}
@ -909,9 +855,10 @@ This keeps the runtime lightweight while making new OpenAI-compatible backends m
"dm_scope": "per-channel-peer",
"backlog_limit": 20
},
"channels": {
"channel_list": {
"telegram": {
"enabled": true"
"enabled": true,
"type": "telegram",
// token: set in .security.yml
"allow_from": ["123456789"]
}

View file

@ -40,9 +40,10 @@ Communiquez avec votre PicoClaw via Telegram, Discord, WhatsApp, Matrix, QQ, Din
```json
{
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"]
}
@ -98,9 +99,10 @@ Vous pouvez aussi gerer les competences installees directement depuis Telegram :
```json
{
"channels": {
"channel_list": {
"discord": {
"enabled": true,
"type": "discord",
"token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"]
}
@ -121,7 +123,7 @@ Par défaut, le bot répond à tous les messages dans un canal de serveur. Pour
```json
{
"channels": {
"channel_list": {
"discord": {
"group_trigger": { "mention_only": true }
}
@ -133,7 +135,7 @@ Vous pouvez également déclencher par préfixes de mots-clés (par ex. `!bot`)
```json
{
"channels": {
"channel_list": {
"discord": {
"group_trigger": { "prefixes": ["!bot"] }
}
@ -162,9 +164,10 @@ PicoClaw peut se connecter à WhatsApp de deux manières :
```json
{
"channels": {
"channel_list": {
"whatsapp": {
"enabled": true,
"type": "whatsapp",
"use_native": true,
"session_store_path": "",
"allow_from": []
@ -196,9 +199,10 @@ Scannez le QR code affiché avec votre application WeChat mobile. Une fois conne
(Optionnel) Ajoutez votre identifiant utilisateur WeChat dans `allow_from` pour restreindre qui peut envoyer des messages au bot :
```json
{
"channels": {
"channel_list": {
"weixin": {
"enabled": true,
"type": "weixin",
"token": "YOUR_TOKEN",
"allow_from": ["YOUR_USER_ID"]
}
@ -227,9 +231,10 @@ QQ Open Platform propose une page de configuration en un clic pour les bots comp
```json
{
"channels": {
"channel_list": {
"qq": {
"enabled": true,
"type": "qq",
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
@ -269,9 +274,10 @@ Si vous préférez créer le bot manuellement :
```json
{
"channels": {
"channel_list": {
"dingtalk": {
"enabled": true,
"type": "dingtalk",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
@ -302,9 +308,10 @@ picoclaw gateway
```json
{
"channels": {
"channel_list": {
"matrix": {
"enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN",
@ -338,9 +345,10 @@ Pour toutes les options (`device_id`, `join_on_invite`, `group_trigger`, `placeh
```json
{
"channels": {
"channel_list": {
"line": {
"enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line",
@ -396,9 +404,10 @@ Voir le [Guide de Configuration WeCom AI Bot](../channels/wecom/wecom_aibot/READ
```json
{
"channels": {
"channel_list": {
"wecom": {
"enabled": true,
"type": "wecom",
"token": "YOUR_TOKEN",
"encoding_aes_key": "YOUR_ENCODING_AES_KEY",
"webhook_url": "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY",
@ -429,7 +438,7 @@ Voir le [Guide de Configuration WeCom AI Bot](../channels/wecom/wecom_aibot/READ
```json
{
"channels": {
"channel_list": {
"wecom_app": {
"enabled": true,
"corp_id": "wwxxxxxxxxxxxxxxxx",
@ -464,7 +473,7 @@ picoclaw gateway
```json
{
"channels": {
"channel_list": {
"wecom_aibot": {
"enabled": true,
"token": "YOUR_TOKEN",
@ -505,9 +514,10 @@ PicoClaw se connecte à Feishu via le mode WebSocket/SDK — aucune URL webhook
```json
{
"channels": {
"channel_list": {
"feishu": {
"enabled": true,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
@ -545,9 +555,10 @@ Pour toutes les options, voir le [Guide de Configuration du Canal Feishu](../cha
```json
{
"channels": {
"channel_list": {
"slack": {
"enabled": true,
"type": "slack",
"bot_token": "xoxb-YOUR-BOT-TOKEN",
"app_token": "xapp-YOUR-APP-TOKEN",
"allow_from": []
@ -572,9 +583,10 @@ picoclaw gateway
```json
{
"channels": {
"channel_list": {
"irc": {
"enabled": true,
"type": "irc",
"server": "irc.libera.chat:6697",
"tls": true,
"nick": "picoclaw-bot",
@ -612,9 +624,10 @@ Installez et exécutez un framework de bot QQ compatible OneBot v11. Activez son
```json
{
"channels": {
"channel_list": {
"onebot": {
"enabled": true,
"type": "onebot",
"ws_url": "ws://127.0.0.1:8080",
"access_token": "",
"allow_from": []
@ -649,9 +662,10 @@ picoclaw gateway
```json
{
"channels": {
"channel_list": {
"maixcam": {
"enabled": true,
"type": "maixcam",
"allow_from": []
}
}

View file

@ -276,7 +276,7 @@ L'ancienne configuration `providers` est **dépréciée** et a été supprimée
```json
{
"version": 2,
"version": 3,
"model_list": [
{
"model_name": "glm-4.7",
@ -362,19 +362,22 @@ picoclaw agent -m "Hello"
"api_key": "gsk_xxx"
}
},
"channels": {
"channel_list": {
"telegram": {
"enabled": true,
"type": "telegram",
"token": "123456:ABC...",
"allow_from": ["123456789"]
},
"discord": {
"enabled": true,
"type": "discord",
"token": "",
"allow_from": [""]
},
"whatsapp": {
"enabled": false,
"type": "whatsapp",
"bridge_url": "ws://localhost:3001",
"use_native": false,
"session_store_path": "",
@ -382,6 +385,7 @@ picoclaw agent -m "Hello"
},
"feishu": {
"enabled": false,
"type": "feishu",
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
@ -390,6 +394,7 @@ picoclaw agent -m "Hello"
},
"qq": {
"enabled": false,
"type": "qq",
"app_id": "",
"app_secret": "",
"allow_from": []

View file

@ -345,6 +345,7 @@ Au lieu de charger tous les outils, le LLM reçoit un outil de recherche léger
},
"slack": {
"enabled": true,
"type": "slack",
"command": "npx",
"args": [
"-y",

Some files were not shown because too many files have changed in this diff Show more