Merge branch 'main' into output

This commit is contained in:
Cytown 2026-04-15 11:28:52 +08:00 committed by GitHub
commit 937e0aa604
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
323 changed files with 20505 additions and 6811 deletions

View file

@ -16,5 +16,5 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Build - name: Build core binaries
run: make build-all run: make build-all

View file

@ -17,29 +17,38 @@ jobs:
with: with:
ref: main ref: main
# 1. 安装指定版本的 Go (可选,但推荐) # 1. Install Go from go.mod
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: go.mod go-version-file: go.mod
# 2. 安装 pnpm - name: Setup pnpm
- name: Install pnpm uses: pnpm/action-setup@v4
run: brew install pnpm with:
version: 10.33.0
run_install: false
# 3. 运行你的 Makefile 编译二进制文件 - name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: pnpm
cache-dependency-path: web/frontend/pnpm-lock.yaml
# 3. Build the application bundle
- name: Build with Make - name: Build with Make
run: make build ARCH=${{ matrix.arch }} && make build-macos-app ARCH=${{ matrix.arch }} run: make build ARCH=${{ matrix.arch }} && make build-macos-app ARCH=${{ matrix.arch }}
# 4. 签名 # 4. Apply ad-hoc signing
- name: Ad-hoc Sign - name: Ad-hoc Sign
run: codesign --force --deep --sign - "build/PicoClaw Launcher.app" run: codesign --force --deep --sign - "build/PicoClaw Launcher.app"
# 5. 安装打包工具 # 5. Install the DMG packaging tool
- name: Install create-dmg - name: Install create-dmg
run: brew install create-dmg run: brew install create-dmg
# 6. 执行打包命令 # 6. Create the DMG
- name: Create DMG - name: Create DMG
run: | run: |
mkdir -p dist mkdir -p dist
@ -54,7 +63,7 @@ jobs:
"dist/picoclaw-${{ matrix.arch }}.dmg" \ "dist/picoclaw-${{ matrix.arch }}.dmg" \
"build/PicoClaw Launcher.app" "build/PicoClaw Launcher.app"
# 7. 上传文件到 GitHub Artifacts (供你下载) # 7. Upload the DMG as a GitHub artifact
- name: Upload DMG - name: Upload DMG
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:

View file

@ -47,13 +47,18 @@ jobs:
with: with:
go-version-file: go.mod 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 - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 22 node-version: 22
cache: pnpm
- name: Setup pnpm cache-dependency-path: web/frontend/pnpm-lock.yaml
run: corepack enable && corepack prepare pnpm@latest --activate
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
@ -75,6 +80,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install zip
run: sudo apt-get install -y zip
- name: Create local tag for GoReleaser - name: Create local tag for GoReleaser
run: git tag "${{ steps.version.outputs.version }}" run: git tag "${{ steps.version.outputs.version }}"
@ -90,6 +98,7 @@ jobs:
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }} GOVERSION: ${{ steps.setup-go.outputs.go-version }}
GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.version }} GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.version }}
INCLUDE_ANDROID_BUNDLE: "true"
NIGHTLY_BUILD: "true" NIGHTLY_BUILD: "true"
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
@ -123,7 +132,7 @@ jobs:
# Collect release artifacts from goreleaser dist/ # Collect release artifacts from goreleaser dist/
ASSETS=() ASSETS=()
for f in dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/checksums.txt; do for f in dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/checksums.txt build/picoclaw-android-universal.zip; do
[ -f "$f" ] && ASSETS+=("$f") [ -f "$f" ] && ASSETS+=("$f")
done done
@ -135,4 +144,3 @@ jobs:
--prerelease \ --prerelease \
--latest=false \ --latest=false \
"${ASSETS[@]}" "${ASSETS[@]}"

View file

@ -65,13 +65,18 @@ jobs:
with: with:
go-version-file: go.mod 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 - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 22 node-version: 22
cache: pnpm
- name: Setup pnpm cache-dependency-path: web/frontend/pnpm-lock.yaml
run: corepack enable && corepack prepare pnpm@latest --activate
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v4
@ -93,6 +98,9 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Install zip
run: sudo apt-get install -y zip
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v7 uses: goreleaser/goreleaser-action@v7
with: with:
@ -104,23 +112,13 @@ jobs:
GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}
GOVERSION: ${{ steps.setup-go.outputs.go-version }} GOVERSION: ${{ steps.setup-go.outputs.go-version }}
INCLUDE_ANDROID_BUNDLE: "true"
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
- name: Build and upload Android arm64
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
sudo apt-get install -y zip
make build-android-bundle
gh release upload "${{ inputs.tag }}" \
build/picoclaw-android-universal.zip \
--clobber
- name: Apply release flags - name: Apply release flags
shell: bash shell: bash
env: env:

View file

@ -9,11 +9,10 @@ git:
before: before:
hooks: hooks:
- go mod tidy
- go generate ./... - go generate ./...
- sh -c 'cd web/frontend && pnpm install && pnpm build:backend' - sh -c 'cd web/frontend && CI=true pnpm install --frozen-lockfile && pnpm build:backend'
- go install github.com/tc-hib/go-winres@latest - sh -c 'GOBIN="$(go env GOPATH)/bin"; mkdir -p "$GOBIN"; go install github.com/tc-hib/go-winres@v0.3.3 && "$GOBIN/go-winres" make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }}'
- go-winres make --in web/backend/winres/winres.json --out web/backend/rsrc --product-version={{ .Version }} --file-version={{ .Version }} - sh -c 'if [ "${INCLUDE_ANDROID_BUNDLE:-}" = "true" ]; then make build-android-bundle; fi'
builds: builds:
- id: picoclaw - id: picoclaw
@ -27,7 +26,7 @@ builds:
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }} - -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }} - -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }} - -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ .Env.GOVERSION }} - -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos: goos:
- linux - linux
- windows - windows
@ -67,6 +66,10 @@ builds:
- stdjson - stdjson
ldflags: ldflags:
- -s -w - -s -w
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos: goos:
- linux - linux
- windows - windows
@ -106,6 +109,10 @@ builds:
- stdjson - stdjson
ldflags: ldflags:
- -s -w - -s -w
- -X github.com/sipeed/picoclaw/pkg/config.Version={{ .Version }}
- -X github.com/sipeed/picoclaw/pkg/config.GitCommit={{ .ShortCommit }}
- -X github.com/sipeed/picoclaw/pkg/config.BuildTime={{ .Date }}
- -X github.com/sipeed/picoclaw/pkg/config.GoVersion={{ with index .Env "GOVERSION" }}{{ . }}{{ else }}unknown{{ end }}
goos: goos:
- linux - linux
- windows - windows
@ -245,6 +252,8 @@ changelog:
release: release:
disable: '{{ isEnvSet "NIGHTLY_BUILD" }}' disable: '{{ isEnvSet "NIGHTLY_BUILD" }}'
extra_files:
- glob: ./build/picoclaw-android-universal.zip
footer: >- footer: >-
--- ---

View file

@ -1,4 +1,4 @@
.PHONY: all build install uninstall clean help test .PHONY: all build install uninstall clean help test build-all
# Build variables # Build variables
BINARY_NAME=picoclaw BINARY_NAME=picoclaw
@ -217,7 +217,9 @@ build-launcher-android-arm64:
@echo "Building picoclaw-launcher for android/arm64..." @echo "Building picoclaw-launcher for android/arm64..."
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@$(MAKE) -C web build-android-arm64 \ @$(MAKE) -C web build-android-arm64 \
OUTPUT="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-android-arm64" OUTPUT_ANDROID_ARM64="$(CURDIR)/$(BUILD_DIR)/picoclaw-launcher-android-arm64" \
GO='$(GO)' \
LDFLAGS='$(LDFLAGS)'
@echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-android-arm64" @echo "Build complete: $(BUILD_DIR)/picoclaw-launcher-android-arm64"
## build-android-bundle: Build core and launcher for all Android architectures and package as universal zip ## build-android-bundle: Build core and launcher for all Android architectures and package as universal zip
@ -240,7 +242,7 @@ build-android-bundle: generate
build-pi-zero: build-linux-arm build-linux-arm64 build-pi-zero: build-linux-arm build-linux-arm64
@echo "Pi Zero 2 W builds: $(BUILD_DIR)/$(BINARY_NAME)-linux-arm (32-bit), $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 (64-bit)" @echo "Pi Zero 2 W builds: $(BUILD_DIR)/$(BINARY_NAME)-linux-arm (32-bit), $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 (64-bit)"
## build-all: Build picoclaw for all platforms ## build-all: Build the picoclaw core binary for all Makefile-managed platforms
build-all: generate build-all: generate
@echo "Building for multiple platforms..." @echo "Building for multiple platforms..."
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@ -257,8 +259,7 @@ build-all: generate
GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR) GOOS=windows GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
GOOS=netbsd GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 ./$(CMD_DIR) GOOS=netbsd GOARCH=amd64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-amd64 ./$(CMD_DIR)
GOOS=netbsd GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 ./$(CMD_DIR) GOOS=netbsd GOARCH=arm64 $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-netbsd-arm64 ./$(CMD_DIR)
@$(MAKE) build-android-bundle @echo "Core builds complete"
@echo "All builds complete"
## install: Install picoclaw to system and copy builtin skills ## install: Install picoclaw to system and copy builtin skills
install: build install: build

View file

@ -167,19 +167,27 @@ Vous pouvez aussi télécharger le binaire pour votre plateforme depuis la page
### Compiler depuis les sources (pour le développement) ### Compiler depuis les sources (pour le développement)
Prérequis :
- Go 1.25+
- Node.js 22+ et pnpm 10.33.0+ pour les builds Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Installer les dépendances frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Compiler le binaire principal # Compiler le binaire principal
make build make build
# Compiler le Web UI Launcher (requis pour le mode WebUI) # Compiler le Web UI Launcher (requis pour le mode WebUI)
make build-launcher make build-launcher
# Compiler pour plusieurs plateformes # Compiler les binaires core pour toutes les plateformes gérées par le Makefile
make build-all make build-all
# Compiler pour Raspberry Pi Zero 2 W (32 bits : make build-linux-arm ; 64 bits : make build-linux-arm64) # Compiler pour Raspberry Pi Zero 2 W (32 bits : make build-linux-arm ; 64 bits : make build-linux-arm64)
@ -619,6 +627,3 @@ Discord : <https://discord.gg/V4sAZ9XWpN>
WeChat : WeChat :
<img src="assets/wechat.png" alt="WeChat group QR code" width="512"> <img src="assets/wechat.png" alt="WeChat group QR code" width="512">

View file

@ -164,19 +164,27 @@ Atau, unduh binary untuk platform Anda dari halaman [GitHub Releases](https://gi
### Build dari source (untuk pengembangan) ### Build dari source (untuk pengembangan)
Prasyarat:
- Go 1.25+
- Node.js 22+ dan pnpm 10.33.0+ untuk build Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Instal dependensi frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Build binary inti # Build binary inti
make build make build
# Build Web UI Launcher (diperlukan untuk mode WebUI) # Build Web UI Launcher (diperlukan untuk mode WebUI)
make build-launcher make build-launcher
# Build untuk berbagai platform # Build binary inti untuk semua platform yang dikelola Makefile
make build-all make build-all
# Build untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Build untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ In alternativa, scarica il binario per la tua piattaforma dalla pagina delle [Gi
### Compila dai sorgenti (per lo sviluppo) ### Compila dai sorgenti (per lo sviluppo)
Prerequisiti:
- Go 1.25+
- Node.js 22+ e pnpm 10.33.0+ per le build Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Installa le dipendenze frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Compila il binario core # Compila il binario core
make build make build
# Compila il Web UI Launcher (necessario per la modalità WebUI) # Compila il Web UI Launcher (necessario per la modalità WebUI)
make build-launcher make build-launcher
# Compila per più piattaforme # Compila i binari core per tutte le piattaforme gestite dal Makefile
make build-all make build-all
# Compila per Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Compila per Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ PicoClaw はほぼすべての Linux デバイスにデプロイできます!
### ソースからビルド(開発用) ### ソースからビルド(開発用)
前提条件:
- Go 1.25+
- Web UI / launcher のビルドには Node.js 22+ と pnpm 10.33.0+ が必要
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# フロントエンド依存関係をインストール
(cd web/frontend && pnpm install --frozen-lockfile)
# コアバイナリをビルド # コアバイナリをビルド
make build make build
# Web UI Launcher をビルドWebUI モードに必要) # Web UI Launcher をビルドWebUI モードに必要)
make build-launcher make build-launcher
# 複数プラットフォーム向けビルド # Makefile が管理するすべてのプラットフォーム向けにコアバイナリをビルド
make build-all make build-all
# Raspberry Pi Zero 2 W 向けビルド32-bit: make build-linux-arm; 64-bit: make build-linux-arm64 # Raspberry Pi Zero 2 W 向けビルド32-bit: make build-linux-arm; 64-bit: make build-linux-arm64

View file

@ -164,19 +164,27 @@ PicoClaw는 사실상 거의 모든 Linux 장치에 배포할 수 있습니다!
### 소스에서 빌드(개발용) ### 소스에서 빌드(개발용)
필수 사항:
- Go 1.25+
- Web UI / launcher 빌드에는 Node.js 22+와 pnpm 10.33.0+가 필요합니다
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# 프런트엔드 의존성 설치
(cd web/frontend && pnpm install --frozen-lockfile)
# 코어 바이너리 빌드 # 코어 바이너리 빌드
make build make build
# WebUI 런처 빌드 (WebUI 모드에 필요) # WebUI 런처 빌드 (WebUI 모드에 필요)
make build-launcher make build-launcher
# 여러 플랫폼용 빌드 # Makefile이 관리하는 모든 플랫폼용 코어 바이너리 빌드
make build-all make build-all
# Raspberry Pi Zero 2 W용 빌드 (32비트: make build-linux-arm, 64비트: make build-linux-arm64) # Raspberry Pi Zero 2 W용 빌드 (32비트: make build-linux-arm, 64비트: make build-linux-arm64)

View file

@ -164,22 +164,32 @@ Alternatively, download the binary for your platform from the [GitHub Releases](
### Build from source (for development) ### Build from source (for development)
Prerequisites:
- Go 1.25+
- Node.js 22+ and pnpm 10.33.0+ for Web UI / launcher builds
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Build core binary # Install frontend dependencies
(cd web/frontend && pnpm install --frozen-lockfile)
# Build the core binary for the current platform
make build make build
# Build Web UI Launcher (required for WebUI mode) # Build the Web UI Launcher (required for WebUI mode)
make build-launcher make build-launcher
# Build for multiple platforms # Build core binaries for all Makefile-managed platforms
make build-all make build-all
# Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Build for Raspberry Pi Zero 2 W
# 32-bit: make build-linux-arm
# 64-bit: make build-linux-arm64
make build-pi-zero make build-pi-zero
# Build and install # Build and install
@ -513,7 +523,7 @@ picoclaw skills search "web scraping"
picoclaw skills install <skill-name> picoclaw skills install <skill-name>
``` ```
**Configure ClawHub token** (optional, for higher rate limits): **Configure skill registries**:
Add to your `config.json`: Add to your `config.json`:
```json ```json
@ -523,6 +533,11 @@ Add to your `config.json`:
"registries": { "registries": {
"clawhub": { "clawhub": {
"auth_token": "your-clawhub-token" "auth_token": "your-clawhub-token"
},
"github": {
"base_url": "https://github.com",
"auth_token": "your-github-token",
"proxy": ""
} }
} }
} }
@ -530,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). For more details, see [Tools Configuration - Skills](docs/tools_configuration.md#skills-tool).
## 🔗 MCP (Model Context Protocol) ## 🔗 MCP (Model Context Protocol)

View file

@ -165,18 +165,26 @@ Muat turun binari untuk platform anda dari halaman [GitHub Releases](https://git
### Bina dari sumber (untuk pembangunan) ### Bina dari sumber (untuk pembangunan)
Prasyarat:
- Go 1.25+
- Node.js 22+ dan pnpm 10.33.0+ untuk binaan Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Pasang dependensi frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Bina binari teras # Bina binari teras
make build make build
# Bina Pelancar Web UI (diperlukan untuk mod WebUI) # Bina Pelancar Web UI (diperlukan untuk mod WebUI)
make build-launcher make build-launcher
# Bina untuk pelbagai platform # Bina binari teras untuk semua platform yang diuruskan oleh Makefile
make build-all make build-all
# Bina untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Bina untuk Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ Alternativamente, baixe o binário para sua plataforma na página de [GitHub Rel
### Compilar a partir do código-fonte (para desenvolvimento) ### Compilar a partir do código-fonte (para desenvolvimento)
Pré-requisitos:
- Go 1.25+
- Node.js 22+ e pnpm 10.33.0+ para builds do Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Instalar dependências do frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Compilar o binário principal # Compilar o binário principal
make build make build
# Compilar o Web UI Launcher (necessário para o modo WebUI) # Compilar o Web UI Launcher (necessário para o modo WebUI)
make build-launcher make build-launcher
# Compilar para múltiplas plataformas # Compilar os binários core para todas as plataformas gerenciadas pelo Makefile
make build-all make build-all
# Compilar para Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Compilar para Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ Ngoài ra, tải binary cho nền tảng của bạn từ trang [GitHub Releases
### Xây dựng từ mã nguồn (để phát triển) ### Xây dựng từ mã nguồn (để phát triển)
Yêu cầu:
- Go 1.25+
- Node.js 22+ và pnpm 10.33.0+ cho các bản build Web UI / launcher
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# Build core binary # Cài đặt dependencies frontend
(cd web/frontend && pnpm install --frozen-lockfile)
# Build binary lõi
make build make build
# Build Web UI Launcher (required for WebUI mode) # Build Web UI Launcher (cần cho chế độ WebUI)
make build-launcher make build-launcher
# Build for multiple platforms # Build các binary lõi cho mọi nền tảng do Makefile quản lý
make build-all make build-all
# Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64) # Build for Raspberry Pi Zero 2 W (32-bit: make build-linux-arm; 64-bit: make build-linux-arm64)

View file

@ -164,19 +164,27 @@ PicoClaw 几乎可以部署在任何 Linux 设备上!
### 从源码构建(开发用) ### 从源码构建(开发用)
前置要求:
- Go 1.25+
- Node.js 22+ 和 pnpm 10.33.0+(用于 Web UI / launcher 构建)
```bash ```bash
git clone https://github.com/sipeed/picoclaw.git git clone https://github.com/sipeed/picoclaw.git
cd picoclaw cd picoclaw
make deps make deps
# 安装前端依赖
(cd web/frontend && pnpm install --frozen-lockfile)
# 构建核心二进制文件 # 构建核心二进制文件
make build make build
# 构建 Web UI LauncherWebUI 模式必需) # 构建 Web UI LauncherWebUI 模式必需)
make build-launcher make build-launcher
# 为多平台构建 # 为 Makefile 管理的所有平台构建核心二进制文件
make build-all make build-all
# 为 Raspberry Pi Zero 2 W 构建32位: make build-linux-arm; 64位: make build-linux-arm64 # 为 Raspberry Pi Zero 2 W 构建32位: make build-linux-arm; 64位: make build-linux-arm64
@ -507,7 +515,7 @@ picoclaw skills search "web scraping"
picoclaw skills install <skill-name> picoclaw skills install <skill-name>
``` ```
**配置 ClawHub token**(可选,用于提高速率限制) **配置 Skills 仓库源**
`config.json` 中添加: `config.json` 中添加:
```json ```json
@ -517,6 +525,11 @@ picoclaw skills install <skill-name>
"registries": { "registries": {
"clawhub": { "clawhub": {
"auth_token": "your-clawhub-token" "auth_token": "your-clawhub-token"
},
"github": {
"base_url": "https://github.com",
"auth_token": "your-github-token",
"proxy": ""
} }
} }
} }
@ -524,6 +537,8 @@ picoclaw skills install <skill-name>
} }
``` ```
`tools.skills.github.*` 已废弃,请改用 `tools.skills.registries.github.*`
更多详情请参阅 [工具配置 - Skills](docs/zh/tools_configuration.md#skills-tool)。 更多详情请参阅 [工具配置 - Skills](docs/zh/tools_configuration.md#skills-tool)。
## 🔗 MCP (Model Context Protocol) ## 🔗 MCP (Model Context Protocol)
@ -616,7 +631,3 @@ Discord: <https://discord.gg/V4sAZ9XWpN>
WeChat: WeChat:
<img src="assets/wechat.png" alt="WeChat group QR code" width="512"> <img src="assets/wechat.png" alt="WeChat group QR code" width="512">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View file

@ -19,6 +19,7 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
) )
const ( const (
@ -155,11 +156,31 @@ func defaultWeComQRFlowOptions(timeout time.Duration) wecomQRFlowOptions {
} }
func applyWeComAuthResult(cfg *config.Config, botInfo wecomQRBotInfo) { func applyWeComAuthResult(cfg *config.Config, botInfo wecomQRBotInfo) {
cfg.Channels.WeCom.Enabled = true bc := cfg.Channels.GetByType(config.ChannelWeCom)
cfg.Channels.WeCom.BotID = botInfo.BotID if bc == nil {
cfg.Channels.WeCom.SetSecret(botInfo.Secret) bc = &config.Channel{Type: config.ChannelWeCom}
if strings.TrimSpace(cfg.Channels.WeCom.WebSocketURL) == "" { cfg.Channels["wecom"] = bc
cfg.Channels.WeCom.WebSocketURL = wecomDefaultWebSocketURL }
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) { func TestApplyWeComAuthResult(t *testing.T) {
cfg := config.DefaultConfig() 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{ applyWeComAuthResult(cfg, wecomQRBotInfo{
BotID: "bot-1", BotID: "bot-1",
Secret: "secret-1", Secret: "secret-1",
}) })
assert.True(t, cfg.Channels.WeCom.Enabled) assert.True(t, wecom.Enabled)
assert.Equal(t, "bot-1", cfg.Channels.WeCom.BotID) assert.Equal(t, "bot-1", weCfg.BotID)
assert.Equal(t, "secret-1", cfg.Channels.WeCom.Secret.String()) assert.Equal(t, "secret-1", weCfg.Secret.String())
assert.Equal(t, wecomDefaultWebSocketURL, cfg.Channels.WeCom.WebSocketURL) assert.Equal(t, wecomDefaultWebSocketURL, weCfg.WebSocketURL)
} }
func TestAuthWeComCmdWithScanner(t *testing.T) { func TestAuthWeComCmdWithScanner(t *testing.T) {
@ -149,9 +155,13 @@ func TestAuthWeComCmdWithScanner(t *testing.T) {
cfg, err := config.LoadConfig(internal.GetConfigPath()) cfg, err := config.LoadConfig(internal.GetConfigPath())
require.NoError(t, err) require.NoError(t, err)
assert.True(t, cfg.Channels.WeCom.Enabled) wecom := cfg.Channels["wecom"]
assert.Equal(t, "bot-1", cfg.Channels.WeCom.BotID) decoded, err := wecom.GetDecoded()
assert.Equal(t, "secret-1", cfg.Channels.WeCom.Secret.String()) require.NoError(t, err)
assert.Equal(t, wecomDefaultWebSocketURL, cfg.Channels.WeCom.WebSocketURL) 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.") 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) return fmt.Errorf("failed to load config: %w", err)
} }
cfg.Channels.Weixin.Enabled = true bc := cfg.Channels.GetByType(config.ChannelWeixin)
cfg.Channels.Weixin.SetToken(token) if bc == nil {
bc = &config.Channel{Type: config.ChannelWeixin}
cfg.Channels[config.ChannelWeixin] = bc
}
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/" const defaultBase = "https://ilinkai.weixin.qq.com/"
if baseURL != "" && baseURL != defaultBase { if baseURL != "" && baseURL != defaultBase {
cfg.Channels.Weixin.BaseURL = baseURL weixinCfg.BaseURL = baseURL
} }
if proxy != "" { if proxy != "" {
cfg.Channels.Weixin.Proxy = proxy weixinCfg.Proxy = proxy
}
}
} }
return config.SaveConfig(cfgPath, cfg) return config.SaveConfig(cfgPath, cfg)

View file

@ -2,19 +2,34 @@ package gateway
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/gateway" "github.com/sipeed/picoclaw/pkg/gateway"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/netbind"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
) )
func resolveGatewayHostOverride(explicit bool, host string) (string, error) {
if !explicit {
return "", nil
}
normalized, err := netbind.NormalizeHostInput(host)
if err != nil {
return "", fmt.Errorf("invalid --host value: %w", err)
}
return normalized, nil
}
func NewGatewayCommand() *cobra.Command { func NewGatewayCommand() *cobra.Command {
var debug bool var debug bool
var noTruncate bool var noTruncate bool
var allowEmpty bool var allowEmpty bool
var host string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "gateway", Use: "gateway",
@ -33,7 +48,25 @@ func NewGatewayCommand() *cobra.Command {
return nil return nil
}, },
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
resolvedHost, err := resolveGatewayHostOverride(cmd.Flags().Changed("host"), host)
if err != nil {
return err
}
if resolvedHost != "" {
prevHost, hadPrev := os.LookupEnv(config.EnvGatewayHost)
if err := os.Setenv(config.EnvGatewayHost, resolvedHost); err != nil {
return fmt.Errorf("failed to set %s: %w", config.EnvGatewayHost, err)
}
defer func() {
if hadPrev {
_ = os.Setenv(config.EnvGatewayHost, prevHost)
return
}
_ = os.Unsetenv(config.EnvGatewayHost)
}()
}
return gateway.Run(debug, internal.GetPicoclawHome(), internal.GetConfigPath(), allowEmpty) return gateway.Run(debug, internal.GetPicoclawHome(), internal.GetConfigPath(), allowEmpty)
}, },
} }
@ -47,6 +80,12 @@ func NewGatewayCommand() *cobra.Command {
false, false,
"Continue starting even when no default model is configured", "Continue starting even when no default model is configured",
) )
cmd.Flags().StringVar(
&host,
"host",
"",
"Host address for gateway binding (overrides gateway.host for this run)",
)
return cmd return cmd
} }

View file

@ -29,4 +29,38 @@ func TestNewGatewayCommand(t *testing.T) {
assert.True(t, cmd.HasFlags()) assert.True(t, cmd.HasFlags())
assert.NotNil(t, cmd.Flags().Lookup("debug")) assert.NotNil(t, cmd.Flags().Lookup("debug"))
assert.NotNil(t, cmd.Flags().Lookup("allow-empty")) assert.NotNil(t, cmd.Flags().Lookup("allow-empty"))
assert.NotNil(t, cmd.Flags().Lookup("host"))
}
func TestResolveGatewayHostOverride(t *testing.T) {
tests := []struct {
name string
explicit bool
host string
wantHost string
wantErr bool
}{
{name: "implicit empty host is allowed", explicit: false, host: "", wantHost: "", wantErr: false},
{name: "explicit empty host rejected", explicit: true, host: " ", wantHost: "", wantErr: true},
{name: "explicit localhost kept", explicit: true, host: " localhost ", wantHost: "localhost", wantErr: false},
{
name: "explicit multi host normalized",
explicit: true,
host: " [::1] , 127.0.0.1 ",
wantHost: "::1,127.0.0.1",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := resolveGatewayHostOverride(tt.explicit, tt.host)
if (err != nil) != tt.wantErr {
t.Fatalf("resolveGatewayHostOverride() err = %v, wantErr %t", err, tt.wantErr)
}
if got != tt.wantHost {
t.Fatalf("resolveGatewayHostOverride() host = %q, want %q", got, tt.wantHost)
}
})
}
} }

View file

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

View file

@ -12,7 +12,6 @@ import (
type deps struct { type deps struct {
workspace string workspace string
installer *skills.SkillInstaller
skillsLoader *skills.SkillsLoader skillsLoader *skills.SkillsLoader
} }
@ -29,15 +28,6 @@ func NewSkillsCommand() *cobra.Command {
} }
d.workspace = cfg.WorkspacePath() 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 // get global config directory and builtin skills directory
globalDir := filepath.Dir(internal.GetConfigPath()) 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) { loaderFn := func() (*skills.SkillsLoader, error) {
if d.skillsLoader == nil { if d.skillsLoader == nil {
return nil, fmt.Errorf("skills loader is not initialized") return nil, fmt.Errorf("skills loader is not initialized")
@ -75,10 +58,10 @@ func NewSkillsCommand() *cobra.Command {
cmd.AddCommand( cmd.AddCommand(
newListCommand(loaderFn), newListCommand(loaderFn),
newInstallCommand(installerFn), newInstallCommand(),
newInstallBuiltinCommand(workspaceFn), newInstallBuiltinCommand(workspaceFn),
newListBuiltinCommand(), newListBuiltinCommand(),
newRemoveCommand(installerFn), newRemoveCommand(),
newSearchCommand(), newSearchCommand(),
newShowCommand(loaderFn), newShowCommand(loaderFn),
) )

View file

@ -2,6 +2,7 @@ package skills
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -11,12 +12,23 @@ import (
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/fileutil"
"github.com/sipeed/picoclaw/pkg/skills" "github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
) )
const skillsSearchMaxResults = 20 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) { func skillsListCmd(loader *skills.SkillsLoader) {
allSkills := loader.ListSkills() 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). // 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) err := utils.ValidateSkillIdentifier(registryName)
if err != nil { if err != nil {
return fmt.Errorf("✗ invalid registry name: %w", err) return fmt.Errorf("✗ invalid registry name: %w", err)
} }
err = utils.ValidateSkillIdentifier(slug) registryMgr := skills.NewRegistryManagerFromToolsConfig(cfg.Tools.Skills)
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,
},
})
registry := registryMgr.GetRegistry(registryName) registry := registryMgr.GetRegistry(registryName)
if registry == nil { if registry == nil {
return fmt.Errorf("✗ registry '%s' not found or not enabled. check your config.json.", registryName) 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() workspace := cfg.WorkspacePath()
targetDir := filepath.Join(workspace, "skills", slug) targetDir := filepath.Join(workspace, "skills", dirName)
if _, err = os.Stat(targetDir); err == nil { 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) 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) 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 { if err != nil {
rmErr := os.RemoveAll(targetDir) rmErr := os.RemoveAll(targetDir)
if rmErr != nil { 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) 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 { 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 != "" { if result.Summary != "" {
fmt.Printf(" %s\n", result.Summary) fmt.Printf(" %s\n", result.Summary)
} }
@ -129,15 +132,51 @@ func skillsInstallFromRegistry(cfg *config.Config, registryName, slug string) er
return nil return nil
} }
func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) { func writeInstalledSkillOriginMeta(targetDir string, meta installedSkillOriginMeta) error {
fmt.Printf("Removing skill '%s'...\n", skillName) data, err := json.MarshalIndent(meta, "", " ")
if err != nil {
if err := installer.Uninstall(skillName); err != nil { return err
fmt.Printf("✗ Failed to remove skill: %v\n", err) }
os.Exit(1) 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) { func skillsInstallBuiltinCmd(workspace string) {
@ -237,21 +276,7 @@ func skillsSearchCmd(query string) {
return return
} }
clawHubConfig := cfg.Tools.Skills.Registries.ClawHub registryMgr := skills.NewRegistryManagerFromToolsConfig(cfg.Tools.Skills)
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,
},
})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() 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/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "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 var registry string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "install", Use: "install",
Short: "Install skill from GitHub", Short: "Install skill from GitHub or a registry",
Example: ` Example: `
picoclaw skills install sipeed/picoclaw-skills/weather picoclaw skills install sipeed/picoclaw-skills/weather
picoclaw skills install --registry clawhub github picoclaw skills install --registry clawhub github
@ -34,21 +33,15 @@ picoclaw skills install --registry clawhub github
return nil return nil
}, },
RunE: func(_ *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
installer, err := installerFn()
if err != nil {
return err
}
if registry != "" {
cfg, err := internal.LoadConfig() cfg, err := internal.LoadConfig()
if err != nil { if err != nil {
return err return err
} }
if registry != "" {
return skillsInstallFromRegistry(cfg, registry, args[0]) 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) { func TestNewInstallSubcommand(t *testing.T) {
cmd := newInstallCommand(nil) cmd := newInstallCommand()
require.NotNil(t, cmd) require.NotNil(t, cmd)
assert.Equal(t, "install", cmd.Use) 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.Nil(t, cmd.Run)
assert.NotNil(t, cmd.RunE) assert.NotNil(t, cmd.RunE)
@ -79,7 +79,7 @@ func TestInstallCommandArgs(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cmd := newInstallCommand(nil) cmd := newInstallCommand()
if tt.registry != "" { if tt.registry != "" {
require.NoError(t, cmd.Flags().Set("registry", tt.registry)) require.NoError(t, cmd.Flags().Set("registry", tt.registry))

View file

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

View file

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

View file

@ -382,9 +382,16 @@
"timeout": 0, "timeout": 0,
"max_zip_size": 0, "max_zip_size": 0,
"max_response_size": 0 "max_response_size": 0
},
"github": {
"enabled": true,
"base_url": "https://github.com",
"auth_token": "",
"proxy": "http://127.0.0.1:7891"
} }
}, },
"github": { "github": {
"base_url": "https://github.com",
"proxy": "http://127.0.0.1:7891", "proxy": "http://127.0.0.1:7891",
"token": "" "token": ""
}, },
@ -465,7 +472,7 @@
}, },
"gateway": { "gateway": {
"_comment": "Default log level is set to 'fatal'. Other available options are 'debug', 'info', 'warn' and 'error'.", "_comment": "Default log level is set to 'fatal'. Other available options are 'debug', 'info', 'warn' and 'error'.",
"host": "127.0.0.1", "host": "localhost",
"port": 18790, "port": 18790,
"hot_reload": false, "hot_reload": false,
"log_level": "fatal" "log_level": "fatal"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,9 +8,10 @@ PicoClaw 通过 LINE Messaging API 配合 Webhook 回调功能实现对 LINE 的
```json ```json
{ {
"channels": { "channel_list": {
"line": { "line": {
"enabled": true, "enabled": true,
"type": "line",
"channel_secret": "YOUR_CHANNEL_SECRET", "channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN", "channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_path": "/webhook/line", "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 ```json
{ {
"channels": { "channel_list": {
"maixcam": { "maixcam": {
"enabled": true, "enabled": true,
"type": "maixcam",
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 18790, "port": 18790,
"allow_from": [] "allow_from": []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,9 +8,10 @@
```json ```json
{ {
"channels": { "channel_list": {
"matrix": { "matrix": {
"enabled": true, "enabled": true,
"type": "matrix",
"homeserver": "https://matrix.org", "homeserver": "https://matrix.org",
"user_id": "@your-bot:matrix.org", "user_id": "@your-bot:matrix.org",
"access_token": "YOUR_MATRIX_ACCESS_TOKEN", "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 ```json
{ {
"channels": { "channel_list": {
"onebot": { "onebot": {
"enabled": true, "enabled": true,
"type": "onebot",
"ws_url": "ws://localhost:8080", "ws_url": "ws://localhost:8080",
"access_token": "", "access_token": "",
"allow_from": [] "allow_from": []

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,9 +8,10 @@ Telegram チャンネルは、Telegram Bot API を使用したロングポーリ
```json ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz", "token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"], "allow_from": ["123456789"],
"proxy": "", "proxy": "",
@ -42,9 +43,10 @@ Telegram チャンネルは、Telegram Bot API を使用したロングポーリ
```json ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN", "token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"], "allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true "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 ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz", "token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"], "allow_from": ["123456789"],
"proxy": "", "proxy": "",
@ -62,9 +63,10 @@ You can set `use_markdown_v2: true` to enable enhanced formatting options. This
```json ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN", "token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"], "allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true "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 ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz", "token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"], "allow_from": ["123456789"],
"proxy": "", "proxy": "",
@ -42,9 +43,10 @@ Você pode definir `use_markdown_v2: true` para habilitar opções de formataç
```json ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN", "token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"], "allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true "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 ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz", "token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"], "allow_from": ["123456789"],
"proxy": "", "proxy": "",
@ -42,9 +43,10 @@ Bạn có thể đặt `use_markdown_v2: true` để bật các tùy chọn đ
```json ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN", "token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"], "allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true "use_markdown_v2": true

View file

@ -8,9 +8,10 @@ Telegram Channel 通过 Telegram 机器人 API 使用长轮询实现基于机器
```json ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz", "token": "123456789:ABCdefGHIjklMNOpqrsTUVwxyz",
"allow_from": ["123456789"], "allow_from": ["123456789"],
"proxy": "", "proxy": "",
@ -62,9 +63,10 @@ explain how to squash the last 3 commits
```json ```json
{ {
"channels": { "channel_list": {
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"type": "telegram",
"token": "YOUR_BOT_TOKEN", "token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"], "allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": true "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 ```json
{ {
"channels": { "channel_list": {
"vk": { "vk": {
"enabled": true, "enabled": true,
"type": "vk",
"token": "NOT_HERE", "token": "NOT_HERE",
"group_id": 123456789, "group_id": 123456789,
"allow_from": ["123456789"], "allow_from": ["123456789"],
@ -120,9 +121,10 @@ VK has a maximum message length of 4000 characters. PicoClaw automatically split
```json ```json
{ {
"channels": { "channel_list": {
"vk": { "vk": {
"enabled": true, "enabled": true,
"type": "vk",
"token": "NOT_HERE", "token": "NOT_HERE",
"group_id": 123456789 "group_id": 123456789
} }
@ -134,9 +136,10 @@ VK has a maximum message length of 4000 characters. PicoClaw automatically split
```json ```json
{ {
"channels": { "channel_list": {
"vk": { "vk": {
"enabled": true, "enabled": true,
"type": "vk",
"token": "NOT_HERE", "token": "NOT_HERE",
"group_id": 123456789, "group_id": 123456789,
"allow_from": ["123456789", "987654321"] "allow_from": ["123456789", "987654321"]
@ -149,9 +152,10 @@ VK has a maximum message length of 4000 characters. PicoClaw automatically split
```json ```json
{ {
"channels": { "channel_list": {
"vk": { "vk": {
"enabled": true, "enabled": true,
"type": "vk",
"token": "NOT_HERE", "token": "NOT_HERE",
"group_id": 123456789, "group_id": 123456789,
"group_trigger": { "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 ```json
{ {
"channels": { "channel_list": {
"wecom": { "wecom": {
"enabled": true, "enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID", "bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET", "secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com", "websocket_url": "wss://openws.work.weixin.qq.com",

View file

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

View file

@ -56,9 +56,10 @@ picoclaw auth wecom --timeout 10m
```json ```json
{ {
"channels": { "channel_list": {
"wecom": { "wecom": {
"enabled": true, "enabled": true,
"type": "wecom",
"bot_id": "YOUR_BOT_ID", "bot_id": "YOUR_BOT_ID",
"secret": "YOUR_SECRET", "secret": "YOUR_SECRET",
"websocket_url": "wss://openws.work.weixin.qq.com", "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 ```json
{ {
"channels": { "channel_list": {
"weixin": { "weixin": {
"enabled": true, "enabled": true,
"type": "weixin",
"token": "YOUR_WEIXIN_TOKEN", "token": "YOUR_WEIXIN_TOKEN",
"allow_from": [ "allow_from": [
"user_id_1", "user_id_1",

View file

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

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