Merge branch 'main' into feat/refactor_skill_config
This commit is contained in:
commit
91d12af9a9
267 changed files with 12512 additions and 5264 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
27
.github/workflows/create_dmg.yml
vendored
27
.github/workflows/create_dmg.yml
vendored
|
|
@ -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:
|
||||||
|
|
|
||||||
18
.github/workflows/nightly.yml
vendored
18
.github/workflows/nightly.yml
vendored
|
|
@ -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[@]}"
|
||||||
|
|
||||||
|
|
|
||||||
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
|
|
@ -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,6 +112,7 @@ 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 }}
|
||||||
|
|
|
||||||
|
|
@ -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: >-
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ Use descriptive branch names, e.g. `fix/telegram-timeout`, `feat/ollama-provider
|
||||||
- Reference the related issue when relevant: `Fix session leak (#123)`.
|
- Reference the related issue when relevant: `Fix session leak (#123)`.
|
||||||
- Keep commits focused. One logical change per commit is preferred.
|
- Keep commits focused. One logical change per commit is preferred.
|
||||||
- For minor cleanups or typo fixes, squash them into a single commit before opening a PR.
|
- For minor cleanups or typo fixes, squash them into a single commit before opening a PR.
|
||||||
- Refer to https://www.conventionalcommits.org/zh-hans/v1.0.0/
|
- Refer to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||||
|
|
||||||
### Keeping Up to Date
|
### Keeping Up to Date
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ git checkout -b 你的功能分支名
|
||||||
- 有关联 Issue 时请引用:`Fix session leak (#123)`。
|
- 有关联 Issue 时请引用:`Fix session leak (#123)`。
|
||||||
- 保持 commit 专注,每个 commit 只做一件事。
|
- 保持 commit 专注,每个 commit 只做一件事。
|
||||||
- 对于小的清理或拼写修正,提 PR 前请将其合并为一个 commit。
|
- 对于小的清理或拼写修正,提 PR 前请将其合并为一个 commit。
|
||||||
- 按照 https://www.conventionalcommits.org/zh-hans/v1.0.0/ 规范来撰写
|
- 按照 [Conventional Commits](https://www.conventionalcommits.org/zh-hans/v1.0.0/) 规范来撰写
|
||||||
|
|
||||||
### 保持与上游同步
|
### 保持与上游同步
|
||||||
|
|
||||||
|
|
|
||||||
39
Makefile
39
Makefile
|
|
@ -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
|
||||||
|
|
@ -205,11 +205,44 @@ build-linux-mipsle: generate
|
||||||
$(call PATCH_MIPS_FLAGS,$(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle)
|
$(call PATCH_MIPS_FLAGS,$(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle)
|
||||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle"
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)-linux-mipsle"
|
||||||
|
|
||||||
|
## build-android-arm64: Build core for Android ARM64
|
||||||
|
build-android-arm64: generate
|
||||||
|
@echo "Building for android/arm64..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-android-arm64 ./$(CMD_DIR)
|
||||||
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)-android-arm64"
|
||||||
|
|
||||||
|
## build-launcher-android-arm64: Build launcher for Android ARM64
|
||||||
|
build-launcher-android-arm64:
|
||||||
|
@echo "Building picoclaw-launcher for android/arm64..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
@$(MAKE) -C web build-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"
|
||||||
|
|
||||||
|
## build-android-bundle: Build core and launcher for all Android architectures and package as universal zip
|
||||||
|
build-android-bundle: generate
|
||||||
|
@echo "Building core for all Android architectures..."
|
||||||
|
@mkdir -p $(BUILD_DIR)
|
||||||
|
GOOS=android GOARCH=arm64 $(GO) build -tags stdjson -ldflags "$(LDFLAGS)" -o $(BUILD_DIR)/$(BINARY_NAME)-android-arm64 ./$(CMD_DIR)
|
||||||
|
@echo "Building launcher for Android arm64..."
|
||||||
|
@$(MAKE) build-launcher-android-arm64
|
||||||
|
@echo "Staging JNI libs..."
|
||||||
|
@rm -rf $(BUILD_DIR)/android-staging
|
||||||
|
@mkdir -p $(BUILD_DIR)/android-staging/arm64-v8a
|
||||||
|
@cp $(BUILD_DIR)/$(BINARY_NAME)-android-arm64 $(BUILD_DIR)/android-staging/arm64-v8a/libpicoclaw.so
|
||||||
|
@cp $(BUILD_DIR)/picoclaw-launcher-android-arm64 $(BUILD_DIR)/android-staging/arm64-v8a/libpicoclaw-web.so
|
||||||
|
@cd $(BUILD_DIR)/android-staging && zip -r ../picoclaw-android-universal.zip .
|
||||||
|
@rm -rf $(BUILD_DIR)/android-staging
|
||||||
|
@echo "All Android builds complete: $(BUILD_DIR)/picoclaw-android-universal.zip"
|
||||||
|
|
||||||
## build-pi-zero: Build for Raspberry Pi Zero 2 W (32-bit and 64-bit)
|
## build-pi-zero: Build for Raspberry Pi Zero 2 W (32-bit and 64-bit)
|
||||||
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)
|
||||||
|
|
@ -226,7 +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)
|
||||||
@echo "All builds complete"
|
@echo "Core builds complete"
|
||||||
|
|
||||||
## install: Install picoclaw to system and copy builtin skills
|
## install: Install picoclaw to system and copy builtin skills
|
||||||
install: build
|
install: build
|
||||||
|
|
|
||||||
13
README.fr.md
13
README.fr.md
|
|
@ -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">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
10
README.id.md
10
README.id.md
|
|
@ -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)
|
||||||
|
|
|
||||||
10
README.it.md
10
README.it.md
|
|
@ -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)
|
||||||
|
|
|
||||||
10
README.ja.md
10
README.ja.md
|
|
@ -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)
|
||||||
|
|
|
||||||
10
README.ko.md
10
README.ko.md
|
|
@ -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)
|
||||||
|
|
|
||||||
24
README.md
24
README.md
|
|
@ -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
|
||||||
|
|
@ -215,7 +225,7 @@ picoclaw-launcher
|
||||||
<img src="assets/launcher-webui.jpg" alt="WebUI Launcher" width="600">
|
<img src="assets/launcher-webui.jpg" alt="WebUI Launcher" width="600">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Getting started:**
|
**Getting started:**
|
||||||
|
|
||||||
Open the WebUI, then: **1)** Configure a Provider (add your LLM API key) -> **2)** Configure a Channel (e.g., Telegram) -> **3)** Start the Gateway -> **4)** Chat!
|
Open the WebUI, then: **1)** Configure a Provider (add your LLM API key) -> **2)** Configure a Channel (e.g., Telegram) -> **3)** Start the Gateway -> **4)** Chat!
|
||||||
|
|
||||||
|
|
@ -293,7 +303,7 @@ picoclaw-launcher-tui
|
||||||
<img src="assets/launcher-tui.jpg" alt="TUI Launcher" width="600">
|
<img src="assets/launcher-tui.jpg" alt="TUI Launcher" width="600">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Getting started:**
|
**Getting started:**
|
||||||
|
|
||||||
Use the TUI menus to: **1)** Configure a Provider -> **2)** Configure a Channel -> **3)** Start the Gateway -> **4)** Chat!
|
Use the TUI menus to: **1)** Configure a Provider -> **2)** Configure a Channel -> **3)** Start the Gateway -> **4)** Chat!
|
||||||
|
|
||||||
|
|
@ -368,7 +378,7 @@ This creates `~/.picoclaw/config.json` and the workspace directory.
|
||||||
```
|
```
|
||||||
|
|
||||||
> See `config/config.example.json` in the repo for a complete configuration template with all available options.
|
> See `config/config.example.json` in the repo for a complete configuration template with all available options.
|
||||||
>
|
>
|
||||||
> Please note: config.example.json format is version 0, with sensitive codes in it, and will be auto migrated to version 1+, then, the config.json will only store insensitive data, the sensitive codes will be stored in .security.yml, if you need manually modify the codes, please see `docs/security_configuration.md` for more details.
|
> Please note: config.example.json format is version 0, with sensitive codes in it, and will be auto migrated to version 1+, then, the config.json will only store insensitive data, the sensitive codes will be stored in .security.yml, if you need manually modify the codes, please see `docs/security_configuration.md` for more details.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
10
README.my.md
10
README.my.md
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
14
README.vi.md
14
README.vi.md
|
|
@ -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)
|
||||||
|
|
|
||||||
10
README.zh.md
10
README.zh.md
|
|
@ -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 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位: make build-linux-arm; 64位: make build-linux-arm64)
|
# 为 Raspberry Pi Zero 2 W 构建(32位: make build-linux-arm; 64位: make build-linux-arm64)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
const defaultBase = "https://ilinkai.weixin.qq.com/"
|
bc = &config.Channel{Type: config.ChannelWeixin}
|
||||||
if baseURL != "" && baseURL != defaultBase {
|
cfg.Channels[config.ChannelWeixin] = bc
|
||||||
cfg.Channels.Weixin.BaseURL = baseURL
|
|
||||||
}
|
}
|
||||||
if proxy != "" {
|
bc.Enabled = true
|
||||||
cfg.Channels.Weixin.Proxy = proxy
|
|
||||||
|
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)
|
return config.SaveConfig(cfgPath, cfg)
|
||||||
|
|
|
||||||
147
cmd/picoclaw/internal/cliui/cliui.go
Normal file
147
cmd/picoclaw/internal/cliui/cliui.go
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Package cliui renders human-oriented CLI output: bordered panels and columns
|
||||||
|
// on wide interactive terminals. Layout (boxes/columns) is independent of ANSI
|
||||||
|
// color: use --no-color or NO_COLOR to disable colors only; narrow or non-TTY
|
||||||
|
// stdout falls back to plain line-oriented output.
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Minimum terminal width (columns) for bordered / structured layout.
|
||||||
|
// Below this, plain line-oriented output is used so boxes do not wrap badly.
|
||||||
|
const minWidthFancy = 88
|
||||||
|
|
||||||
|
// Minimum width to lay out some views in two columns (e.g. status providers).
|
||||||
|
const minWidthColumns = 104
|
||||||
|
|
||||||
|
var initMu sync.Mutex
|
||||||
|
|
||||||
|
// Init configures lipgloss for this process. When disableAnsiColors is true
|
||||||
|
// (e.g. --no-color, NO_COLOR, or TERM=dumb), only color is turned off; Unicode
|
||||||
|
// borders still render when UseFancyLayout() is true.
|
||||||
|
func Init(disableAnsiColors bool) {
|
||||||
|
initMu.Lock()
|
||||||
|
defer initMu.Unlock()
|
||||||
|
if disableAnsiColors {
|
||||||
|
lipgloss.SetColorProfile(termenv.Ascii)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lipgloss.SetColorProfile(termenv.EnvColorProfile())
|
||||||
|
}
|
||||||
|
|
||||||
|
// StdoutWidth returns the terminal width or a sane default if unknown.
|
||||||
|
func StdoutWidth() int {
|
||||||
|
w, _, err := term.GetSize(int(os.Stdout.Fd()))
|
||||||
|
if err != nil || w < 20 {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFancyLayout is true when styled boxes/columns should be used.
|
||||||
|
func UseFancyLayout() bool {
|
||||||
|
if !term.IsTerminal(int(os.Stdout.Fd())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return StdoutWidth() >= minWidthFancy
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseColumnLayout is true when a second content column is viable.
|
||||||
|
func UseColumnLayout() bool {
|
||||||
|
return UseFancyLayout() && StdoutWidth() >= minWidthColumns
|
||||||
|
}
|
||||||
|
|
||||||
|
// InnerWidth is the target content width inside borders/margins.
|
||||||
|
func InnerWidth() int {
|
||||||
|
w := StdoutWidth()
|
||||||
|
// Rounded border + horizontal padding (lipgloss borders ~= 2 cols each side + padding).
|
||||||
|
const borderBudget = 8
|
||||||
|
if w > borderBudget+48 {
|
||||||
|
return w - borderBudget
|
||||||
|
}
|
||||||
|
return 48
|
||||||
|
}
|
||||||
|
|
||||||
|
// StderrWidth returns stderr terminal width or a sane default.
|
||||||
|
func StderrWidth() int {
|
||||||
|
w, _, err := term.GetSize(int(os.Stderr.Fd()))
|
||||||
|
if err != nil || w < 20 {
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseFancyStderr is true when stderr can show boxed errors without ugly wraps.
|
||||||
|
func UseFancyStderr() bool {
|
||||||
|
if !term.IsTerminal(int(os.Stderr.Fd())) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return StderrWidth() >= minWidthFancy
|
||||||
|
}
|
||||||
|
|
||||||
|
// InnerStderrWidth mirrors InnerWidth but for stderr.
|
||||||
|
func InnerStderrWidth() int {
|
||||||
|
w := StderrWidth()
|
||||||
|
const borderBudget = 8
|
||||||
|
if w > borderBudget+48 {
|
||||||
|
return w - borderBudget
|
||||||
|
}
|
||||||
|
return 48
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
accentBlue = lipgloss.Color("#3E5DB9")
|
||||||
|
accentRed = lipgloss.Color("#D54646")
|
||||||
|
colorMuted = lipgloss.Color("#6B6B6B")
|
||||||
|
colorOK = lipgloss.Color("#2E7D32")
|
||||||
|
)
|
||||||
|
|
||||||
|
func borderStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(accentBlue).
|
||||||
|
Padding(0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func titleBarStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Foreground(accentRed).
|
||||||
|
Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mutedStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(colorMuted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bodyStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func kvKeyStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func kvValStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpIntroStyle is the top tagline (PicoClaw blue, matches ASCII banner left side).
|
||||||
|
func helpIntroStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpIdentStyle is the left column for commands and flags (blue identifiers).
|
||||||
|
func helpIdentStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentBlue).Bold(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// helpPlaceholderStyle highlights <placeholders> in usage lines (red accent).
|
||||||
|
func helpPlaceholderStyle() lipgloss.Style {
|
||||||
|
return lipgloss.NewStyle().Foreground(accentRed).Bold(true)
|
||||||
|
}
|
||||||
180
cmd/picoclaw/internal/cliui/cliui_test.go
Normal file
180
cmd/picoclaw/internal/cliui/cliui_test.go
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Disable ANSI colors in tests so output is predictable plain text.
|
||||||
|
Init(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// showErrHint
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestShowErrHint(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
msg string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
// Cobra flag errors — should show hint
|
||||||
|
{"unknown flag: --foo", true},
|
||||||
|
{"unknown shorthand flag: 'f' in -f", true},
|
||||||
|
{"flag needs an argument: --output", true},
|
||||||
|
{"required flag(s) \"model\" not set", true},
|
||||||
|
// Generic invalid-argument errors — should show hint
|
||||||
|
{"invalid argument \"abc\" for --count", true},
|
||||||
|
// required flag errors — should show hint
|
||||||
|
{"required flag(s) \"model\" not set", true},
|
||||||
|
// usage: in message — should show hint
|
||||||
|
{"bad input\nusage: picoclaw ...", true},
|
||||||
|
// Should NOT false-positive on broad words
|
||||||
|
{"connection flagged by remote", false},
|
||||||
|
{"feature flag not set", false},
|
||||||
|
{"invalid API key provided", false},
|
||||||
|
{"authentication required", false},
|
||||||
|
// Unrelated messages — no hint
|
||||||
|
{"something went wrong", false},
|
||||||
|
{"network timeout", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
got := showErrHint(tc.msg)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("showErrHint(%q) = %v, want %v", tc.msg, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// styleUsageTokens
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestStyleUsageTokensContainsTokens(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
contains []string // substrings that must appear in plain output
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"picoclaw agent <message>",
|
||||||
|
[]string{"picoclaw agent", "<message>"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"picoclaw [command] [flags]",
|
||||||
|
[]string{"picoclaw", "[command]", "[flags]"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"picoclaw",
|
||||||
|
[]string{"picoclaw"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cmd <arg1> [--flag]",
|
||||||
|
[]string{"cmd", "<arg1>", "[--flag]"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
out := styleUsageTokens(tc.input)
|
||||||
|
for _, sub := range tc.contains {
|
||||||
|
if !containsStripped(out, sub) {
|
||||||
|
t.Errorf("styleUsageTokens(%q): output %q does not contain %q", tc.input, out, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsStripped checks whether plain contains sub after stripping ANSI escapes.
|
||||||
|
// Since Init(true) sets Ascii profile, lipgloss emits no escape codes in tests,
|
||||||
|
// so this is just a plain substring check.
|
||||||
|
func containsStripped(plain, sub string) bool {
|
||||||
|
return len(plain) >= len(sub) && findSubstring(plain, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findSubstring(s, sub string) bool {
|
||||||
|
for i := 0; i <= len(s)-len(sub); i++ {
|
||||||
|
if s[i:i+len(sub)] == sub {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// collectFlagRows
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
func TestCollectFlagRows_Empty(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
rows := collectFlagRows(fs)
|
||||||
|
if len(rows) != 0 {
|
||||||
|
t.Fatalf("expected 0 rows for empty FlagSet, got %d", len(rows))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectFlagRows_BasicFlags(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
fs.String("output", "", "output file path")
|
||||||
|
fs.Bool("verbose", false, "enable verbose mode")
|
||||||
|
fs.Int("count", 1, "number of items")
|
||||||
|
|
||||||
|
rows := collectFlagRows(fs)
|
||||||
|
|
||||||
|
if len(rows) != 3 {
|
||||||
|
t.Fatalf("expected 3 rows, got %d", len(rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rows must be sorted alphabetically by flag name.
|
||||||
|
names := make([]string, 0, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
names = append(names, r[0])
|
||||||
|
}
|
||||||
|
if names[0] > names[1] || names[1] > names[2] {
|
||||||
|
t.Errorf("rows not sorted: %v", names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectFlagRows_Shorthand(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
fs.StringP("model", "m", "", "model name")
|
||||||
|
|
||||||
|
rows := collectFlagRows(fs)
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 row, got %d", len(rows))
|
||||||
|
}
|
||||||
|
left := rows[0][0]
|
||||||
|
if !findSubstring(left, "-m") || !findSubstring(left, "--model") {
|
||||||
|
t.Errorf("expected shorthand and long form in %q", left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectFlagRows_HiddenFlagsExcluded(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
fs.String("visible", "", "this shows up")
|
||||||
|
hidden := fs.String("hidden", "", "this should not show up")
|
||||||
|
_ = hidden
|
||||||
|
_ = fs.MarkHidden("hidden")
|
||||||
|
|
||||||
|
rows := collectFlagRows(fs)
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 row (hidden excluded), got %d", len(rows))
|
||||||
|
}
|
||||||
|
if !findSubstring(rows[0][0], "visible") {
|
||||||
|
t.Errorf("expected visible flag in rows, got %q", rows[0][0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectFlagRows_UsageInRightColumn(t *testing.T) {
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
fs.String("format", "json", "output format: json or text")
|
||||||
|
|
||||||
|
rows := collectFlagRows(fs)
|
||||||
|
if len(rows) != 1 {
|
||||||
|
t.Fatalf("expected 1 row, got %d", len(rows))
|
||||||
|
}
|
||||||
|
if rows[0][1] != "output format: json or text" {
|
||||||
|
t.Errorf("expected usage in right column, got %q", rows[0][1])
|
||||||
|
}
|
||||||
|
}
|
||||||
298
cmd/picoclaw/internal/cliui/help_cmd.go
Normal file
298
cmd/picoclaw/internal/cliui/help_cmd.go
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderCommandHelp builds Ruff-style sectioned, two-column help when
|
||||||
|
// UseFancyLayout(); otherwise plain Cobra-style text.
|
||||||
|
func RenderCommandHelp(c *cobra.Command) string {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
return plainCommandHelp(c)
|
||||||
|
}
|
||||||
|
syncFlags(c)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
head, sub := helpIntro(c)
|
||||||
|
if head != "" {
|
||||||
|
b.WriteString(helpIntroStyle().Render(head))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
if sub != "" {
|
||||||
|
b.WriteString(mutedStyle().Render(sub))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
if head != "" || sub != "" {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := InnerWidth()
|
||||||
|
contentW := inner - 6
|
||||||
|
if contentW < 36 {
|
||||||
|
contentW = 36
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
usageBody := bodyStyle().MaxWidth(contentW).Render(styleUsageTokens(c.UseLine()))
|
||||||
|
b.WriteString(sectionPanel("Usage", usageBody, inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
// Examples
|
||||||
|
if ex := strings.TrimSpace(c.Example); ex != "" {
|
||||||
|
exBody := bodyStyle().Width(contentW).Render(ex)
|
||||||
|
b.WriteString(sectionPanel("Examples", exBody, inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subcommands
|
||||||
|
subs := visibleSubcommands(c)
|
||||||
|
if len(subs) > 0 {
|
||||||
|
rows := make([][2]string, 0, len(subs))
|
||||||
|
for _, sub := range subs {
|
||||||
|
left := sub.Name()
|
||||||
|
if a := sub.Aliases; len(a) > 0 {
|
||||||
|
left += " (" + strings.Join(a, ", ") + ")"
|
||||||
|
}
|
||||||
|
rows = append(rows, [2]string{left, sub.Short})
|
||||||
|
}
|
||||||
|
b.WriteString(sectionPanel("Commands", renderTwoColPairs(rows, contentW), inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local options
|
||||||
|
local := c.LocalFlags()
|
||||||
|
opts := collectFlagRows(local)
|
||||||
|
if len(opts) > 0 {
|
||||||
|
title := "Options"
|
||||||
|
if !c.HasParent() {
|
||||||
|
title = "Flags"
|
||||||
|
}
|
||||||
|
b.WriteString(sectionPanel(title, renderTwoColPairs(opts, contentW), inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global (inherited) options
|
||||||
|
if c.HasAvailableInheritedFlags() {
|
||||||
|
inh := collectFlagRows(c.InheritedFlags())
|
||||||
|
if len(inh) > 0 {
|
||||||
|
b.WriteString(sectionPanel("Global options", renderTwoColPairs(inh, contentW), inner))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCommandQuickRef prints the same Usage / Flags / Global sections as help,
|
||||||
|
// for embedding after errors (stderr). outerW is typically InnerStderrWidth().
|
||||||
|
func RenderCommandQuickRef(c *cobra.Command, outerW int) string {
|
||||||
|
if c == nil || outerW < 40 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
syncFlags(c)
|
||||||
|
contentW := outerW - 6
|
||||||
|
if contentW < 36 {
|
||||||
|
contentW = 36
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
usageBody := bodyStyle().MaxWidth(contentW).Render(styleUsageTokens(c.UseLine()))
|
||||||
|
b.WriteString(sectionPanel("Usage", usageBody, outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
if len(c.Aliases) > 0 {
|
||||||
|
al := "Aliases: " + strings.Join(c.Aliases, ", ")
|
||||||
|
alBody := mutedStyle().MaxWidth(contentW).Render(al)
|
||||||
|
b.WriteString(sectionPanel("Aliases", alBody, outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
opts := collectFlagRows(c.LocalFlags())
|
||||||
|
if len(opts) > 0 {
|
||||||
|
title := "Options"
|
||||||
|
if !c.HasParent() {
|
||||||
|
title = "Flags"
|
||||||
|
}
|
||||||
|
b.WriteString(sectionPanel(title, renderTwoColPairs(opts, contentW), outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
if c.HasAvailableInheritedFlags() {
|
||||||
|
inh := collectFlagRows(c.InheritedFlags())
|
||||||
|
if len(inh) > 0 {
|
||||||
|
b.WriteString(sectionPanel("Global options", renderTwoColPairs(inh, contentW), outerW))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncFlags(c *cobra.Command) {
|
||||||
|
_ = c.LocalFlags()
|
||||||
|
if c.HasAvailableInheritedFlags() {
|
||||||
|
_ = c.InheritedFlags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func plainCommandHelp(c *cobra.Command) string {
|
||||||
|
desc := c.Long
|
||||||
|
if desc == "" {
|
||||||
|
desc = c.Short
|
||||||
|
}
|
||||||
|
desc = strings.TrimRight(desc, " \t\n\r")
|
||||||
|
var b strings.Builder
|
||||||
|
if desc != "" {
|
||||||
|
fmt.Fprintln(&b, desc)
|
||||||
|
fmt.Fprintln(&b)
|
||||||
|
}
|
||||||
|
if c.Runnable() || c.HasSubCommands() {
|
||||||
|
b.WriteString(c.UsageString())
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpIntro(c *cobra.Command) (head, sub string) {
|
||||||
|
head = strings.TrimSpace(c.Short)
|
||||||
|
long := strings.TrimSpace(c.Long)
|
||||||
|
if long == "" || long == head {
|
||||||
|
return head, ""
|
||||||
|
}
|
||||||
|
lines := strings.Split(long, "\n")
|
||||||
|
var rest []string
|
||||||
|
for i, ln := range lines {
|
||||||
|
ln = strings.TrimSpace(ln)
|
||||||
|
if ln == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == 0 && ln == head {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest = append(rest, ln)
|
||||||
|
}
|
||||||
|
sub = strings.Join(rest, "\n")
|
||||||
|
return head, sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleSubcommands(c *cobra.Command) []*cobra.Command {
|
||||||
|
var out []*cobra.Command
|
||||||
|
for _, sub := range c.Commands() {
|
||||||
|
if sub.Hidden {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, sub)
|
||||||
|
}
|
||||||
|
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func sectionPanel(title, body string, width int) string {
|
||||||
|
head := titleBarStyle().Render(title) + "\n\n"
|
||||||
|
return borderStyle().Width(width).Render(head + body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// styleUsageTokens highlights PicoClaw-blue command tokens and red <placeholders>/[groups].
|
||||||
|
func styleUsageTokens(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for len(s) > 0 {
|
||||||
|
ia := strings.Index(s, "<")
|
||||||
|
ib := strings.Index(s, "[")
|
||||||
|
next, kind := -1, 0 // 1 = angle, 2 = bracket
|
||||||
|
switch {
|
||||||
|
case ia >= 0 && (ib < 0 || ia < ib):
|
||||||
|
next, kind = ia, 1
|
||||||
|
case ib >= 0:
|
||||||
|
next, kind = ib, 2
|
||||||
|
}
|
||||||
|
if next < 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if next > 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s[:next]))
|
||||||
|
}
|
||||||
|
s = s[next:]
|
||||||
|
if kind == 1 {
|
||||||
|
j := strings.Index(s, ">")
|
||||||
|
if j < 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteString(helpPlaceholderStyle().Render(s[:j+1]))
|
||||||
|
s = s[j+1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
j := strings.Index(s, "]")
|
||||||
|
if j < 0 {
|
||||||
|
b.WriteString(helpIdentStyle().Render(s))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.WriteString(helpPlaceholderStyle().Render(s[:j+1]))
|
||||||
|
s = s[j+1:]
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectFlagRows(fs *flag.FlagSet) [][2]string {
|
||||||
|
var names []string
|
||||||
|
seen := map[string][2]string{}
|
||||||
|
fs.VisitAll(func(f *flag.Flag) {
|
||||||
|
if f.Hidden {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
left := formatFlagLeft(f)
|
||||||
|
right := f.Usage
|
||||||
|
if f.Deprecated != "" {
|
||||||
|
right += " (deprecated: " + f.Deprecated + ")"
|
||||||
|
}
|
||||||
|
names = append(names, f.Name)
|
||||||
|
seen[f.Name] = [2]string{left, right}
|
||||||
|
})
|
||||||
|
sort.Strings(names)
|
||||||
|
rows := make([][2]string, 0, len(names))
|
||||||
|
for _, n := range names {
|
||||||
|
rows = append(rows, seen[n])
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatFlagLeft(f *flag.Flag) string {
|
||||||
|
if len(f.Shorthand) > 0 {
|
||||||
|
return "-" + f.Shorthand + ", --" + f.Name
|
||||||
|
}
|
||||||
|
return "--" + f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderTwoColPairs(rows [][2]string, contentW int) string {
|
||||||
|
if len(rows) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
leftW := 0
|
||||||
|
for _, r := range rows {
|
||||||
|
if w := lipgloss.Width(r[0]); w > leftW {
|
||||||
|
leftW = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const minLeft, maxLeft = 16, 34
|
||||||
|
if leftW < minLeft {
|
||||||
|
leftW = minLeft
|
||||||
|
}
|
||||||
|
if leftW > maxLeft {
|
||||||
|
leftW = maxLeft
|
||||||
|
}
|
||||||
|
gap := " "
|
||||||
|
rightW := contentW - leftW - lipgloss.Width(gap)
|
||||||
|
if rightW < 24 {
|
||||||
|
rightW = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range rows {
|
||||||
|
left := helpIdentStyle().Width(leftW).Align(lipgloss.Left).Render(r[0])
|
||||||
|
right := bodyStyle().Width(rightW).Render(strings.TrimSpace(r[1]))
|
||||||
|
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, left, gap, right))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return strings.TrimRight(b.String(), "\n")
|
||||||
|
}
|
||||||
75
cmd/picoclaw/internal/cliui/help_error.go
Normal file
75
cmd/picoclaw/internal/cliui/help_error.go
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatCLIError formats errors with the same boxed sections as help. When ctx
|
||||||
|
// is the command that was running when the error occurred, Usage / Flags panels
|
||||||
|
// are appended so styling matches picoclaw -h.
|
||||||
|
func FormatCLIError(msg string, ctx *cobra.Command) string {
|
||||||
|
msg = strings.TrimRight(msg, "\n")
|
||||||
|
if !UseFancyStderr() {
|
||||||
|
s := "Error: " + msg + "\n"
|
||||||
|
if ctx != nil && showErrHint(msg) {
|
||||||
|
s += "\n" + plainCommandHelp(ctx)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
w := InnerStderrWidth()
|
||||||
|
contentW := w - 6
|
||||||
|
if contentW < 36 {
|
||||||
|
contentW = 36
|
||||||
|
}
|
||||||
|
|
||||||
|
title := titleBarStyle().Render("Error") + "\n\n"
|
||||||
|
|
||||||
|
paras := strings.Split(msg, "\n")
|
||||||
|
var body strings.Builder
|
||||||
|
for i, p := range paras {
|
||||||
|
p = strings.TrimRight(p, " ")
|
||||||
|
if p == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
st := bodyStyle().Width(contentW)
|
||||||
|
if i > 0 {
|
||||||
|
body.WriteString("\n")
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
body.WriteString(st.Render(p))
|
||||||
|
} else {
|
||||||
|
body.WriteString(mutedStyle().Width(contentW).Render(p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foot := ""
|
||||||
|
if showErrHint(msg) {
|
||||||
|
if ctx != nil {
|
||||||
|
foot = "\n\n" + mutedStyle().Width(contentW).
|
||||||
|
Render("Full command help: "+ctx.CommandPath()+" --help")
|
||||||
|
} else {
|
||||||
|
foot = "\n\n" + mutedStyle().Width(contentW).
|
||||||
|
Render("Tip: picoclaw --help · picoclaw <command> --help")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := borderStyle().Width(w).Render(title+body.String()+foot) + "\n"
|
||||||
|
if ctx != nil && showErrHint(msg) {
|
||||||
|
if ref := RenderCommandQuickRef(ctx, w); ref != "" {
|
||||||
|
out += "\n" + ref
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func showErrHint(msg string) bool {
|
||||||
|
m := strings.ToLower(msg)
|
||||||
|
return strings.Contains(m, "unknown flag") ||
|
||||||
|
strings.Contains(m, "unknown shorthand flag") ||
|
||||||
|
strings.Contains(m, "flag needs an argument") ||
|
||||||
|
strings.Contains(m, "invalid argument") ||
|
||||||
|
strings.Contains(m, "required flag") ||
|
||||||
|
strings.Contains(m, "usage:")
|
||||||
|
}
|
||||||
110
cmd/picoclaw/internal/cliui/onboard.go
Normal file
110
cmd/picoclaw/internal/cliui/onboard.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintOnboardComplete prints the post-onboard “ready” message and next steps.
|
||||||
|
func PrintOnboardComplete(logo string, encrypt bool, configPath string) {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
printOnboardPlain(logo, encrypt, configPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printOnboardFancy(logo, encrypt, configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printOnboardPlain(logo string, encrypt bool, configPath string) {
|
||||||
|
fmt.Printf("\n%s picoclaw is ready!\n", logo)
|
||||||
|
fmt.Println("\nNext steps:")
|
||||||
|
if encrypt {
|
||||||
|
fmt.Println(" 1. Set your encryption passphrase before starting picoclaw:")
|
||||||
|
fmt.Println(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS")
|
||||||
|
fmt.Println(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println(" 2. Add your API key to", configPath)
|
||||||
|
} else {
|
||||||
|
fmt.Println(" 1. Add your API key to", configPath)
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println(" Recommended:")
|
||||||
|
fmt.Println(" - OpenRouter: https://openrouter.ai/keys (access 100+ models)")
|
||||||
|
fmt.Println(" - Ollama: https://ollama.com (local, free)")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println(" See README.md for 17+ supported providers.")
|
||||||
|
fmt.Println("")
|
||||||
|
if encrypt {
|
||||||
|
fmt.Println(" 3. Chat: picoclaw agent -m \"Hello!\"")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printOnboardFancy(logo string, encrypt bool, configPath string) {
|
||||||
|
inner := InnerWidth()
|
||||||
|
box := borderStyle().MaxWidth(inner + 8)
|
||||||
|
|
||||||
|
ready := titleBarStyle().Render(logo+" picoclaw is ready!") + "\n"
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(box.Width(inner).Render(strings.TrimSpace(ready)))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
steps := buildOnboardingSteps(encrypt, configPath)
|
||||||
|
rec := recommendedBlock()
|
||||||
|
chat := chatStep(encrypt)
|
||||||
|
|
||||||
|
if UseColumnLayout() {
|
||||||
|
leftW := min(inner/2-2, 52)
|
||||||
|
rightW := inner - leftW - 4
|
||||||
|
if rightW < 36 {
|
||||||
|
rightW = 36
|
||||||
|
}
|
||||||
|
leftBlock := borderStyle().MaxWidth(leftW + 8).Width(leftW).
|
||||||
|
Render(titleBarStyle().Render("Next steps") + "\n\n" + bodyStyle().Width(leftW).Render(steps))
|
||||||
|
rightBlock := borderStyle().MaxWidth(rightW + 8).Width(rightW).
|
||||||
|
Render(mutedStyle().Bold(true).Render("Recommended") + "\n\n" + bodyStyle().Width(rightW).Render(rec))
|
||||||
|
gap := strings.Repeat(" ", 2)
|
||||||
|
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, leftBlock, gap, rightBlock))
|
||||||
|
fmt.Println()
|
||||||
|
full := borderStyle().Width(inner).Render(bodyStyle().Width(inner - 4).Render(chat))
|
||||||
|
fmt.Println(full)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same order as plain output: numbered steps → recommended → chat line.
|
||||||
|
next := titleBarStyle().Render("Next steps") + "\n\n" +
|
||||||
|
bodyStyle().Width(inner-4).Render(steps+"\n\n"+rec+"\n\n"+chat)
|
||||||
|
fmt.Println(borderStyle().Width(inner).Render(next))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOnboardingSteps(encrypt bool, configPath string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
if encrypt {
|
||||||
|
b.WriteString("1. Set your encryption passphrase before starting picoclaw:\n")
|
||||||
|
b.WriteString(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS\n")
|
||||||
|
b.WriteString(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd\n\n")
|
||||||
|
b.WriteString("2. Add your API key to\n ")
|
||||||
|
b.WriteString(configPath)
|
||||||
|
b.WriteString("\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString("1. Add your API key to\n ")
|
||||||
|
b.WriteString(configPath)
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func recommendedBlock() string {
|
||||||
|
return "• OpenRouter: https://openrouter.ai/keys\n (access 100+ models)\n\n" +
|
||||||
|
"• Ollama: https://ollama.com\n (local, free)\n\n" +
|
||||||
|
"See README.md for 17+ supported providers."
|
||||||
|
}
|
||||||
|
|
||||||
|
func chatStep(encrypt bool) string {
|
||||||
|
if encrypt {
|
||||||
|
return "3. Chat:\n picoclaw agent -m \"Hello!\""
|
||||||
|
}
|
||||||
|
return "2. Chat:\n picoclaw agent -m \"Hello!\""
|
||||||
|
}
|
||||||
168
cmd/picoclaw/internal/cliui/status.go
Normal file
168
cmd/picoclaw/internal/cliui/status.go
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProviderRow holds one provider's display name and status value.
|
||||||
|
type ProviderRow struct {
|
||||||
|
Name string
|
||||||
|
Val string
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusReport is a structured status view for PrintStatus.
|
||||||
|
type StatusReport struct {
|
||||||
|
Logo string
|
||||||
|
Version string
|
||||||
|
Build string
|
||||||
|
ConfigPath string
|
||||||
|
ConfigOK bool
|
||||||
|
WorkspacePath string
|
||||||
|
WorkspaceOK bool
|
||||||
|
Model string
|
||||||
|
Providers []ProviderRow
|
||||||
|
OAuthLines []string // each full line "provider (method): state"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintStatus renders picoclaw status (plain or fancy).
|
||||||
|
func PrintStatus(r StatusReport) {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
printStatusPlain(r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
printStatusFancy(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatusPlain(r StatusReport) {
|
||||||
|
fmt.Printf("%s picoclaw Status\n", r.Logo)
|
||||||
|
fmt.Printf("Version: %s\n", r.Version)
|
||||||
|
if r.Build != "" {
|
||||||
|
fmt.Printf("Build: %s\n", r.Build)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
printPathLine("Config", r.ConfigPath, r.ConfigOK)
|
||||||
|
printPathLine("Workspace", r.WorkspacePath, r.WorkspaceOK)
|
||||||
|
|
||||||
|
if r.ConfigOK {
|
||||||
|
fmt.Printf("Model: %s\n", r.Model)
|
||||||
|
for _, p := range r.Providers {
|
||||||
|
fmt.Printf("%s: %s\n", p.Name, p.Val)
|
||||||
|
}
|
||||||
|
if len(r.OAuthLines) > 0 {
|
||||||
|
fmt.Println("\nOAuth/Token Auth:")
|
||||||
|
for _, line := range r.OAuthLines {
|
||||||
|
fmt.Printf(" %s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printPathLine(label, path string, ok bool) {
|
||||||
|
mark := "✗"
|
||||||
|
if ok {
|
||||||
|
mark = "✓"
|
||||||
|
}
|
||||||
|
fmt.Println(label+":", path, mark)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatusFancy(r StatusReport) {
|
||||||
|
inner := InnerWidth()
|
||||||
|
topBox := borderStyle().Width(inner)
|
||||||
|
|
||||||
|
var head strings.Builder
|
||||||
|
head.WriteString(titleBarStyle().Render(r.Logo + " picoclaw Status"))
|
||||||
|
head.WriteString("\n\n")
|
||||||
|
head.WriteString(kvKeyStyle().Render("Version") + " " + kvValStyle().Render(r.Version))
|
||||||
|
if r.Build != "" {
|
||||||
|
head.WriteString("\n")
|
||||||
|
head.WriteString(kvKeyStyle().Render("Build") + " " + kvValStyle().Render(r.Build))
|
||||||
|
}
|
||||||
|
fmt.Println(topBox.Render(head.String()))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if UseColumnLayout() && len(r.Providers) > 0 && r.ConfigOK {
|
||||||
|
leftW := (inner - 2) / 2
|
||||||
|
rightW := inner - leftW - 2
|
||||||
|
pathsNarrow := pathStatusPanel(r, leftW)
|
||||||
|
prov := providerTablePanel(r, rightW)
|
||||||
|
gap := strings.Repeat(" ", 2)
|
||||||
|
fmt.Println(lipgloss.JoinHorizontal(lipgloss.Top, pathsNarrow, gap, prov))
|
||||||
|
} else {
|
||||||
|
fmt.Println(pathStatusPanel(r, inner))
|
||||||
|
if len(r.Providers) > 0 && r.ConfigOK {
|
||||||
|
fmt.Println(providerTablePanel(r, inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.OAuthLines) > 0 && r.ConfigOK {
|
||||||
|
var ob strings.Builder
|
||||||
|
ob.WriteString(titleBarStyle().Render("OAuth / token auth") + "\n\n")
|
||||||
|
for _, line := range r.OAuthLines {
|
||||||
|
ob.WriteString(" • " + line + "\n")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(borderStyle().Width(inner).Render(ob.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathStatusPanel(r StatusReport, inner int) string {
|
||||||
|
cfgMark := statusMark(r.ConfigOK)
|
||||||
|
wsMark := statusMark(r.WorkspaceOK)
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(kvKeyStyle().Render("Config") + "\n")
|
||||||
|
b.WriteString(mutedStyle().Render(r.ConfigPath))
|
||||||
|
b.WriteString(" " + cfgMark + "\n\n")
|
||||||
|
b.WriteString(kvKeyStyle().Render("Workspace") + "\n")
|
||||||
|
b.WriteString(mutedStyle().Render(r.WorkspacePath))
|
||||||
|
b.WriteString(" " + wsMark + "\n")
|
||||||
|
if r.ConfigOK {
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(kvKeyStyle().Render("Model") + " " + kvValStyle().Render(r.Model))
|
||||||
|
}
|
||||||
|
return borderStyle().Width(inner).Render(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusMark(ok bool) string {
|
||||||
|
if ok {
|
||||||
|
return lipgloss.NewStyle().Foreground(colorOK).Render("✓")
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().Foreground(accentRed).Render("✗")
|
||||||
|
}
|
||||||
|
|
||||||
|
func providerTablePanel(r StatusReport, colW int) string {
|
||||||
|
if len(r.Providers) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
keyW := min(22, colW/3)
|
||||||
|
if keyW < 14 {
|
||||||
|
keyW = 14
|
||||||
|
}
|
||||||
|
valW := colW - keyW - 3
|
||||||
|
if valW < 12 {
|
||||||
|
valW = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(titleBarStyle().Render("Providers & local") + "\n\n")
|
||||||
|
for _, p := range r.Providers {
|
||||||
|
k := lipgloss.NewStyle().Foreground(accentBlue).Bold(true).Width(keyW).Render(p.Name)
|
||||||
|
v := styleProviderVal(p.Val).Width(valW).Render(p.Val)
|
||||||
|
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, k, " ", v))
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
return borderStyle().Width(colW).Render(strings.TrimRight(b.String(), "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func styleProviderVal(s string) lipgloss.Style {
|
||||||
|
if s == "✓" || strings.HasPrefix(s, "✓ ") {
|
||||||
|
return lipgloss.NewStyle().Foreground(colorOK)
|
||||||
|
}
|
||||||
|
if s == "not set" {
|
||||||
|
return mutedStyle()
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
}
|
||||||
61
cmd/picoclaw/internal/cliui/version.go
Normal file
61
cmd/picoclaw/internal/cliui/version.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
package cliui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintVersion prints version, optional build info, and Go toolchain line.
|
||||||
|
func PrintVersion(logo, versionLine string, build, goVer string) {
|
||||||
|
if !UseFancyLayout() {
|
||||||
|
fmt.Printf("%s %s\n", logo, versionLine)
|
||||||
|
if build != "" {
|
||||||
|
fmt.Printf(" Build: %s\n", build)
|
||||||
|
}
|
||||||
|
if goVer != "" {
|
||||||
|
fmt.Printf(" Go: %s\n", goVer)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inner := InnerWidth()
|
||||||
|
box := borderStyle().Width(inner)
|
||||||
|
|
||||||
|
if UseColumnLayout() {
|
||||||
|
leftCol := kvKeyStyle().Width(12).Align(lipgloss.Right)
|
||||||
|
rightW := inner - 16
|
||||||
|
rightStyle := kvValStyle().Width(rightW)
|
||||||
|
|
||||||
|
rows := [][]string{
|
||||||
|
{leftCol.Render("Version"), rightStyle.Render(versionLine)},
|
||||||
|
}
|
||||||
|
if build != "" {
|
||||||
|
rows = append(rows, []string{leftCol.Render("Build"), rightStyle.Render(build)})
|
||||||
|
}
|
||||||
|
if goVer != "" {
|
||||||
|
rows = append(rows, []string{leftCol.Render("Go"), rightStyle.Render(goVer)})
|
||||||
|
}
|
||||||
|
var body strings.Builder
|
||||||
|
for _, r := range rows {
|
||||||
|
body.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, r[0], " ", r[1]))
|
||||||
|
body.WriteString("\n")
|
||||||
|
}
|
||||||
|
header := titleBarStyle().Render(logo+" picoclaw") + "\n\n"
|
||||||
|
fmt.Println(box.Render(header + body.String()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
lines = append(lines, titleBarStyle().Render(logo+" picoclaw"))
|
||||||
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, kvKeyStyle().Render("Version")+" "+kvValStyle().Render(versionLine))
|
||||||
|
if build != "" {
|
||||||
|
lines = append(lines, kvKeyStyle().Render("Build")+" "+kvValStyle().Render(build))
|
||||||
|
}
|
||||||
|
if goVer != "" {
|
||||||
|
lines = append(lines, kvKeyStyle().Render("Go")+" "+kvValStyle().Render(goVer))
|
||||||
|
}
|
||||||
|
fmt.Println(box.Render(strings.Join(lines, "\n")))
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/credential"
|
"github.com/sipeed/picoclaw/pkg/credential"
|
||||||
)
|
)
|
||||||
|
|
@ -79,29 +80,7 @@ func onboard(encrypt bool) {
|
||||||
workspace := cfg.WorkspacePath()
|
workspace := cfg.WorkspacePath()
|
||||||
createWorkspaceTemplates(workspace)
|
createWorkspaceTemplates(workspace)
|
||||||
|
|
||||||
fmt.Printf("\n%s picoclaw is ready!\n", internal.Logo)
|
cliui.PrintOnboardComplete(internal.Logo, encrypt, configPath)
|
||||||
fmt.Println("\nNext steps:")
|
|
||||||
if encrypt {
|
|
||||||
fmt.Println(" 1. Set your encryption passphrase before starting picoclaw:")
|
|
||||||
fmt.Println(" export PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Linux/macOS")
|
|
||||||
fmt.Println(" set PICOCLAW_KEY_PASSPHRASE=<your-passphrase> # Windows cmd")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println(" 2. Add your API key to", configPath)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" 1. Add your API key to", configPath)
|
|
||||||
}
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println(" Recommended:")
|
|
||||||
fmt.Println(" - OpenRouter: https://openrouter.ai/keys (access 100+ models)")
|
|
||||||
fmt.Println(" - Ollama: https://ollama.com (local, free)")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println(" See README.md for 17+ supported providers.")
|
|
||||||
fmt.Println("")
|
|
||||||
if encrypt {
|
|
||||||
fmt.Println(" 3. Chat: picoclaw agent -m \"Hello!\"")
|
|
||||||
} else {
|
|
||||||
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// promptPassphrase reads the encryption passphrase twice from the terminal
|
// promptPassphrase reads the encryption passphrase twice from the terminal
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ package status
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/pkg/auth"
|
"github.com/sipeed/picoclaw/pkg/auth"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
@ -17,43 +19,125 @@ func statusCmd() {
|
||||||
}
|
}
|
||||||
|
|
||||||
configPath := internal.GetConfigPath()
|
configPath := internal.GetConfigPath()
|
||||||
|
|
||||||
fmt.Printf("%s picoclaw Status\n", internal.Logo)
|
|
||||||
fmt.Printf("Version: %s\n", config.FormatVersion())
|
|
||||||
build, _ := config.FormatBuildInfo()
|
build, _ := config.FormatBuildInfo()
|
||||||
if build != "" {
|
|
||||||
fmt.Printf("Build: %s\n", build)
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
_, configStatErr := os.Stat(configPath)
|
||||||
fmt.Println("Config:", configPath, "✓")
|
configOK := configStatErr == nil
|
||||||
} else {
|
|
||||||
fmt.Println("Config:", configPath, "✗")
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace := cfg.WorkspacePath()
|
workspace := cfg.WorkspacePath()
|
||||||
if _, err := os.Stat(workspace); err == nil {
|
_, wsErr := os.Stat(workspace)
|
||||||
fmt.Println("Workspace:", workspace, "✓")
|
wsOK := wsErr == nil
|
||||||
} else {
|
|
||||||
fmt.Println("Workspace:", workspace, "✗")
|
report := cliui.StatusReport{
|
||||||
|
Logo: internal.Logo,
|
||||||
|
Version: config.FormatVersion(),
|
||||||
|
Build: build,
|
||||||
|
ConfigPath: configPath,
|
||||||
|
ConfigOK: configOK,
|
||||||
|
WorkspacePath: workspace,
|
||||||
|
WorkspaceOK: wsOK,
|
||||||
|
Model: cfg.Agents.Defaults.GetModelName(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if configOK {
|
||||||
fmt.Printf("Model: %s\n", cfg.Agents.Defaults.GetModelName())
|
// PicoClaw moved to a model-centric configuration (model_list). Status should
|
||||||
|
// not depend on a legacy cfg.Providers field (which may not exist under some
|
||||||
|
// build tags). We infer provider availability from model_list entries.
|
||||||
|
hasProtocolKey := func(protocol string) bool {
|
||||||
|
prefix := protocol + "/"
|
||||||
|
for _, m := range cfg.ModelList {
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(m.Model, prefix) && m.APIKey() != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
findLocalModelBase := func(modelName string) (string, bool) {
|
||||||
|
for _, m := range cfg.ModelList {
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if m.ModelName == modelName && m.APIBase != "" {
|
||||||
|
return m.APIBase, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
findProtocolBase := func(protocol string) (string, bool) {
|
||||||
|
prefix := protocol + "/"
|
||||||
|
for _, m := range cfg.ModelList {
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(m.Model, prefix) && m.APIBase != "" {
|
||||||
|
return m.APIBase, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
hasOpenRouter := hasProtocolKey("openrouter")
|
||||||
|
hasAnthropic := hasProtocolKey("anthropic")
|
||||||
|
hasOpenAI := hasProtocolKey("openai")
|
||||||
|
hasGemini := hasProtocolKey("gemini")
|
||||||
|
hasZhipu := hasProtocolKey("zhipu")
|
||||||
|
hasQwen := hasProtocolKey("qwen")
|
||||||
|
hasGroq := hasProtocolKey("groq")
|
||||||
|
hasMoonshot := hasProtocolKey("moonshot")
|
||||||
|
hasDeepSeek := hasProtocolKey("deepseek")
|
||||||
|
hasVolcEngine := hasProtocolKey("volcengine")
|
||||||
|
hasNvidia := hasProtocolKey("nvidia")
|
||||||
|
|
||||||
|
// Local endpoints: allow both the special reserved name and protocol-based entries.
|
||||||
|
vllmBase, hasVLLM := findLocalModelBase("local-model")
|
||||||
|
if !hasVLLM {
|
||||||
|
vllmBase, hasVLLM = findProtocolBase("vllm")
|
||||||
|
}
|
||||||
|
ollamaBase, hasOllama := findProtocolBase("ollama")
|
||||||
|
|
||||||
|
val := func(enabled bool, extra ...string) string {
|
||||||
|
if enabled {
|
||||||
|
if len(extra) > 0 && extra[0] != "" {
|
||||||
|
return "✓ " + extra[0]
|
||||||
|
}
|
||||||
|
return "✓"
|
||||||
|
}
|
||||||
|
return "not set"
|
||||||
|
}
|
||||||
|
|
||||||
|
report.Providers = []cliui.ProviderRow{
|
||||||
|
{Name: "OpenRouter API", Val: val(hasOpenRouter)},
|
||||||
|
{Name: "Anthropic API", Val: val(hasAnthropic)},
|
||||||
|
{Name: "OpenAI API", Val: val(hasOpenAI)},
|
||||||
|
{Name: "Gemini API", Val: val(hasGemini)},
|
||||||
|
{Name: "Zhipu API", Val: val(hasZhipu)},
|
||||||
|
{Name: "Qwen API", Val: val(hasQwen)},
|
||||||
|
{Name: "Groq API", Val: val(hasGroq)},
|
||||||
|
{Name: "Moonshot API", Val: val(hasMoonshot)},
|
||||||
|
{Name: "DeepSeek API", Val: val(hasDeepSeek)},
|
||||||
|
{Name: "VolcEngine API", Val: val(hasVolcEngine)},
|
||||||
|
{Name: "Nvidia API", Val: val(hasNvidia)},
|
||||||
|
{Name: "vLLM / local", Val: val(hasVLLM, vllmBase)},
|
||||||
|
{Name: "Ollama", Val: val(hasOllama, ollamaBase)},
|
||||||
|
}
|
||||||
|
|
||||||
store, _ := auth.LoadStore()
|
store, _ := auth.LoadStore()
|
||||||
if store != nil && len(store.Credentials) > 0 {
|
if store != nil && len(store.Credentials) > 0 {
|
||||||
fmt.Println("\nOAuth/Token Auth:")
|
|
||||||
for provider, cred := range store.Credentials {
|
for provider, cred := range store.Credentials {
|
||||||
status := "authenticated"
|
st := "authenticated"
|
||||||
if cred.IsExpired() {
|
if cred.IsExpired() {
|
||||||
status = "expired"
|
st = "expired"
|
||||||
} else if cred.NeedsRefresh() {
|
} else if cred.NeedsRefresh() {
|
||||||
status = "needs refresh"
|
st = "needs refresh"
|
||||||
}
|
}
|
||||||
fmt.Printf(" %s (%s): %s\n", provider, cred.AuthMethod, status)
|
report.OAuthLines = append(report.OAuthLines,
|
||||||
|
fmt.Sprintf("%s (%s): %s", provider, cred.AuthMethod, st))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cliui.PrintStatus(report)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"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/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -23,12 +22,6 @@ func NewVersionCommand() *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
func printVersion() {
|
func printVersion() {
|
||||||
fmt.Printf("%s picoclaw %s\n", internal.Logo, config.FormatVersion())
|
|
||||||
build, goVer := config.FormatBuildInfo()
|
build, goVer := config.FormatBuildInfo()
|
||||||
if build != "" {
|
cliui.PrintVersion(internal.Logo, "picoclaw "+config.FormatVersion(), build, goVer)
|
||||||
fmt.Printf(" Build: %s\n", build)
|
|
||||||
}
|
|
||||||
if goVer != "" {
|
|
||||||
fmt.Printf(" Go: %s\n", goVer)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/agent"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/auth"
|
||||||
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cliui"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/cron"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/gateway"
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal/migrate"
|
||||||
|
|
@ -28,15 +29,57 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/updater"
|
"github.com/sipeed/picoclaw/pkg/updater"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var rootNoColor bool
|
||||||
|
|
||||||
|
func syncCliUIColor(root *cobra.Command) {
|
||||||
|
no, _ := root.PersistentFlags().GetBool("no-color")
|
||||||
|
cliui.Init(no || os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb")
|
||||||
|
}
|
||||||
|
|
||||||
|
// earlyColorDisabled matches lipgloss/banner behavior from env and argv before Cobra parses flags.
|
||||||
|
func earlyColorDisabled() bool {
|
||||||
|
if os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i := 1; i < len(os.Args); i++ {
|
||||||
|
arg := os.Args[i]
|
||||||
|
if arg == "--no-color" || arg == "--no-color=true" || arg == "--no-color=1" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func NewPicoclawCommand() *cobra.Command {
|
func NewPicoclawCommand() *cobra.Command {
|
||||||
short := fmt.Sprintf("%s picoclaw - Personal AI Assistant %s\n\n", internal.Logo, config.GetVersion())
|
short := fmt.Sprintf("%s PicoClaw — personal AI assistant", internal.Logo)
|
||||||
|
long := fmt.Sprintf(`%s PicoClaw is a lightweight personal AI assistant.
|
||||||
|
|
||||||
|
Version: %s`, internal.Logo, config.FormatVersion())
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "picoclaw",
|
Use: "picoclaw",
|
||||||
Short: short,
|
Short: short,
|
||||||
Example: "picoclaw version",
|
Long: long,
|
||||||
|
Example: `picoclaw version
|
||||||
|
picoclaw onboard
|
||||||
|
picoclaw --no-color status`,
|
||||||
|
SilenceErrors: true,
|
||||||
|
// Avoid plain UsageString() on stderr/stdout when a command fails; cliui
|
||||||
|
// renders matching panels on stderr instead.
|
||||||
|
SilenceUsage: true,
|
||||||
|
PersistentPreRun: func(c *cobra.Command, _ []string) {
|
||||||
|
syncCliUIColor(c.Root())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.PersistentFlags().BoolVar(&rootNoColor, "no-color", false,
|
||||||
|
"Disable colors (boxed layout unchanged)")
|
||||||
|
|
||||||
|
cmd.SetHelpFunc(func(c *cobra.Command, _ []string) {
|
||||||
|
syncCliUIColor(c.Root())
|
||||||
|
fmt.Fprint(c.OutOrStdout(), cliui.RenderCommandHelp(c))
|
||||||
|
})
|
||||||
|
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
onboard.NewOnboardCommand(),
|
onboard.NewOnboardCommand(),
|
||||||
agent.NewAgentCommand(),
|
agent.NewAgentCommand(),
|
||||||
|
|
@ -65,17 +108,31 @@ const (
|
||||||
colorBlue + "██║ ██║╚██████╗╚██████╔╝" + colorRed + "╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
|
colorBlue + "██║ ██║╚██████╗╚██████╔╝" + colorRed + "╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
|
||||||
colorBlue + "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ " + colorRed + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n " +
|
colorBlue + "╚═╝ ╚═╝ ╚═════╝ ╚═════╝ " + colorRed + " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n " +
|
||||||
"\033[0m\r\n"
|
"\033[0m\r\n"
|
||||||
|
plainBanner = "\r\n" +
|
||||||
|
"██████╗ ██╗ ██████╗ ██████╗ ██████╗██╗ █████╗ ██╗ ██╗\n" +
|
||||||
|
"██╔══██╗██║██╔════╝██╔═══██╗██╔════╝██║ ██╔══██╗██║ ██║\n" +
|
||||||
|
"██████╔╝██║██║ ██║ ██║██║ ██║ ███████║██║ █╗ ██║\n" +
|
||||||
|
"██╔═══╝ ██║██║ ██║ ██║██║ ██║ ██╔══██║██║███╗██║\n" +
|
||||||
|
"██║ ██║╚██████╗╚██████╔╝╚██████╗███████╗██║ ██║╚███╔███╔╝\n" +
|
||||||
|
"╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝\n " +
|
||||||
|
"\r\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Printf("%s", banner)
|
cliui.Init(earlyColorDisabled())
|
||||||
|
|
||||||
tz_env := os.Getenv("TZ")
|
if earlyColorDisabled() {
|
||||||
if tz_env != "" {
|
fmt.Print(plainBanner)
|
||||||
fmt.Println("TZ environment:", tz_env)
|
} else {
|
||||||
zoneinfo_env := os.Getenv("ZONEINFO")
|
fmt.Printf("%s", banner)
|
||||||
fmt.Println("ZONEINFO environment:", zoneinfo_env)
|
}
|
||||||
loc, err := time.LoadLocation(tz_env)
|
|
||||||
|
tzEnv := os.Getenv("TZ")
|
||||||
|
if tzEnv != "" {
|
||||||
|
fmt.Println("TZ environment:", tzEnv)
|
||||||
|
zoneinfoEnv := os.Getenv("ZONEINFO")
|
||||||
|
fmt.Println("ZONEINFO environment:", zoneinfoEnv)
|
||||||
|
loc, err := time.LoadLocation(tzEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error loading time zone:", err)
|
fmt.Println("Error loading time zone:", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -85,7 +142,10 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := NewPicoclawCommand()
|
cmd := NewPicoclawCommand()
|
||||||
if err := cmd.Execute(); err != nil {
|
last, err := cmd.ExecuteC()
|
||||||
|
if err != nil {
|
||||||
|
syncCliUIColor(cmd)
|
||||||
|
fmt.Fprint(os.Stderr, cliui.FormatCLIError(err.Error(), last))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
@ -17,20 +18,22 @@ func TestNewPicoclawCommand(t *testing.T) {
|
||||||
|
|
||||||
require.NotNil(t, cmd)
|
require.NotNil(t, cmd)
|
||||||
|
|
||||||
short := fmt.Sprintf("%s picoclaw - Personal AI Assistant %s\n\n", internal.Logo, config.GetVersion())
|
short := fmt.Sprintf("%s PicoClaw — personal AI assistant", internal.Logo)
|
||||||
|
longHas := strings.Contains(cmd.Long, config.FormatVersion())
|
||||||
|
|
||||||
assert.Equal(t, "picoclaw", cmd.Use)
|
assert.Equal(t, "picoclaw", cmd.Use)
|
||||||
assert.Equal(t, short, cmd.Short)
|
assert.Equal(t, short, cmd.Short)
|
||||||
|
assert.True(t, longHas)
|
||||||
|
|
||||||
assert.True(t, cmd.HasSubCommands())
|
assert.True(t, cmd.HasSubCommands())
|
||||||
assert.True(t, cmd.HasAvailableSubCommands())
|
assert.True(t, cmd.HasAvailableSubCommands())
|
||||||
|
|
||||||
assert.False(t, cmd.HasFlags())
|
assert.True(t, cmd.PersistentFlags().Lookup("no-color") != nil)
|
||||||
|
|
||||||
assert.Nil(t, cmd.Run)
|
assert.Nil(t, cmd.Run)
|
||||||
assert.Nil(t, cmd.RunE)
|
assert.Nil(t, cmd.RunE)
|
||||||
|
|
||||||
assert.Nil(t, cmd.PersistentPreRun)
|
assert.NotNil(t, cmd.PersistentPreRun)
|
||||||
assert.Nil(t, cmd.PersistentPostRun)
|
assert.Nil(t, cmd.PersistentPostRun)
|
||||||
|
|
||||||
allowedCommands := []string{
|
allowedCommands := []string{
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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": "",
|
||||||
|
|
|
||||||
|
|
@ -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": "",
|
||||||
|
|
|
||||||
|
|
@ -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": "",
|
||||||
|
|
|
||||||
|
|
@ -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": "",
|
||||||
|
|
|
||||||
|
|
@ -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": "",
|
||||||
|
|
|
||||||
|
|
@ -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": "",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": [],
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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": []
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue