Merge branch 'main' into feat/suppoer_btw_command
This commit is contained in:
commit
22f170450a
279 changed files with 17416 additions and 6613 deletions
9
.github/workflows/create_dmg.yml
vendored
9
.github/workflows/create_dmg.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
9
.github/workflows/nightly.yml
vendored
9
.github/workflows/nightly.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
17
README.zh.md
17
README.zh.md
|
|
@ -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">
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
191
cmd/picoclaw/internal/skills/helpers_test.go
Normal file
191
cmd/picoclaw/internal/skills/helpers_test.go
Normal 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))
|
||||
}
|
||||
|
|
@ -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])
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestNewRemoveSubcommand(t *testing.T) {
|
||||
cmd := newRemoveCommand(nil)
|
||||
cmd := newRemoveCommand()
|
||||
|
||||
require.NotNil(t, cmd)
|
||||
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@
|
|||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"channel_list": {
|
||||
"feishu": {
|
||||
"enabled": true,
|
||||
"type": "feishu",
|
||||
"app_id": "cli_xxx",
|
||||
"app_secret": "xxx",
|
||||
"encrypt_key": "",
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@
|
|||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"channel_list": {
|
||||
"feishu": {
|
||||
"enabled": true,
|
||||
"type": "feishu",
|
||||
"app_id": "cli_xxx",
|
||||
"app_secret": "xxx",
|
||||
"encrypt_key": "",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ OneBot は QQ ボット向けのオープンプロトコル標準で、複数の
|
|||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"channel_list": {
|
||||
"onebot": {
|
||||
"enabled": true,
|
||||
"type": "onebot",
|
||||
"ws_url": "ws://localhost:8080",
|
||||
"access_token": "",
|
||||
"allow_from": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ Slack は世界をリードする企業向けインスタントメッセージ
|
|||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"channel_list": {
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"type": "slack",
|
||||
"bot_token": "xoxb-...",
|
||||
"app_token": "xapp-...",
|
||||
"allow_from": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ Slack 是全球领先的企业级即时通讯平台。PicoClaw 采用 Slack 的
|
|||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"channel_list": {
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"type": "slack",
|
||||
"bot_token": "xoxb-...",
|
||||
"app_token": "xapp-...",
|
||||
"allow_from": []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -29,9 +29,10 @@ picoclaw gateway
|
|||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"channel_list": {
|
||||
"weixin": {
|
||||
"enabled": true,
|
||||
"type": "weixin",
|
||||
"token": "YOUR_WEIXIN_TOKEN",
|
||||
"allow_from": [
|
||||
"user_id_1",
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue