Merge branch 'main' of https://github.com/kunalk16/picoclaw into feat-azure-openai-support
This commit is contained in:
commit
0d720f0ad9
35 changed files with 816 additions and 728 deletions
144
.github/workflows/nightly.yml
vendored
144
.github/workflows/nightly.yml
vendored
|
|
@ -9,64 +9,37 @@ permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-tag:
|
nightly:
|
||||||
name: Create Git Tag
|
name: Nightly Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
outputs:
|
packages: write
|
||||||
version: ${{ steps.version.outputs.version }}
|
|
||||||
tag: ${{ steps.version.outputs.tag }}
|
|
||||||
changelog: ${{ steps.version.outputs.changelog }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Generate and push tag
|
- name: Compute version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
DATE=$(date -u +%Y%m%d)
|
DATE=$(date -u +%Y%m%d)
|
||||||
SHA=$(git rev-parse --short=8 HEAD)
|
SHA=$(git rev-parse --short=8 HEAD)
|
||||||
BASE_VERSION=$(git describe --tags --match "v*" --exclude "*nightly*" --abbrev=0 2>/dev/null || true)
|
BASE_VERSION=$(git describe --tags --match "v*" --exclude "*nightly*" --abbrev=0 2>/dev/null || true)
|
||||||
if [ -z "$BASE_VERSION" ] || [ "$BASE_VERSION" = "v0.0.0" ]; then
|
if [ -z "$BASE_VERSION" ] || [ "$BASE_VERSION" = "v0.0.0" ]; then
|
||||||
TAG="v0.0.0-nightly.${DATE}.${SHA}"
|
VERSION="v0.0.0-nightly.${DATE}.${SHA}"
|
||||||
else
|
else
|
||||||
TAG="${BASE_VERSION}-nightly.${DATE}.${SHA}"
|
VERSION="${BASE_VERSION}-nightly.${DATE}.${SHA}"
|
||||||
fi
|
fi
|
||||||
VERSION=$TAG
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
|
|
||||||
echo "Tag $TAG already exists, reusing existing tag"
|
|
||||||
else
|
|
||||||
git tag -a "$TAG" -m "Nightly build $VERSION"
|
|
||||||
fi
|
|
||||||
git push origin "$TAG"
|
|
||||||
|
|
||||||
COMPARE_URL="https://github.com/${{ github.repository }}/commits/${TAG}"
|
COMPARE_URL="https://github.com/${{ github.repository }}/commits/main"
|
||||||
if [ -n "$BASE_VERSION" ] && [ "$BASE_VERSION" != "v0.0.0" ]; then
|
if [ -n "$BASE_VERSION" ] && [ "$BASE_VERSION" != "v0.0.0" ]; then
|
||||||
COMPARE_URL="https://github.com/${{ github.repository }}/compare/${BASE_VERSION}...${TAG}"
|
COMPARE_URL="https://github.com/${{ github.repository }}/compare/${BASE_VERSION}...main"
|
||||||
fi
|
fi
|
||||||
echo "changelog=**Full Changelog**: $COMPARE_URL" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "changelog=**Full Changelog**: $COMPARE_URL" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
release:
|
|
||||||
name: GoReleaser Release
|
|
||||||
needs: create-tag
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout tag
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ needs.create-tag.outputs.tag }}
|
|
||||||
|
|
||||||
- name: Setup Go from go.mod
|
- name: Setup Go from go.mod
|
||||||
id: setup-go
|
id: setup-go
|
||||||
|
|
@ -95,6 +68,16 @@ jobs:
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: docker.io
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Create local tag for GoReleaser
|
||||||
|
run: git tag "${{ steps.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
|
|
@ -106,6 +89,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 }}
|
||||||
|
GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.version }}
|
||||||
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 }}
|
||||||
|
|
@ -113,26 +97,14 @@ jobs:
|
||||||
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
|
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
|
||||||
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
|
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
|
||||||
|
|
||||||
update-rolling:
|
|
||||||
name: Update Rolling Nightly
|
|
||||||
needs: [create-tag, release]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Update nightly release
|
- name: Update nightly release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG: ${{ needs.create-tag.outputs.tag }}
|
VERSION: ${{ steps.version.outputs.version }}
|
||||||
TITLE: ${{ needs.create-tag.outputs.version }}
|
|
||||||
run: |
|
run: |
|
||||||
CHANGELOG='${{ needs.create-tag.outputs.changelog }}'
|
CHANGELOG='${{ steps.version.outputs.changelog }}'
|
||||||
NOTES=$(cat <<EOF
|
NOTES=$(cat <<EOF
|
||||||
Nightly build for **${TITLE}**
|
Nightly build for **${VERSION}**
|
||||||
|
|
||||||
This is an automated build and may be unstable. Use with caution.
|
This is an automated build and may be unstable. Use with caution.
|
||||||
|
|
||||||
|
|
@ -140,65 +112,27 @@ jobs:
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
# Download assets from the newly created release if it exists,
|
# Delete existing nightly release and tag
|
||||||
# otherwise fall back to using locally built dist/ artifacts.
|
gh release delete nightly --cleanup-tag -y 2>/dev/null || true
|
||||||
mkdir -p build
|
|
||||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
||||||
echo "Downloading assets from GitHub release for $TAG..."
|
|
||||||
gh release download "$TAG" --dir build
|
|
||||||
else
|
|
||||||
echo "GitHub release for $TAG not found; falling back to local dist/ artifacts..."
|
|
||||||
if [ -d "dist" ]; then
|
|
||||||
cp -R dist/* build/
|
|
||||||
else
|
|
||||||
echo "Error: no GitHub release for $TAG and no local dist/ directory found." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Delete existing nightly release and tag to avoid conflicts
|
# Force-update nightly tag to current HEAD
|
||||||
echo "Deleting existing nightly release and tag..."
|
git config user.name "github-actions[bot]"
|
||||||
gh release delete nightly --cleanup-tag -y || true
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
git push origin :refs/tags/nightly || true
|
git tag -fa nightly -m "Nightly build ${VERSION}"
|
||||||
|
git push origin nightly
|
||||||
|
|
||||||
|
# Collect release artifacts from goreleaser dist/
|
||||||
|
ASSETS=()
|
||||||
|
for f in dist/*.tar.gz dist/*.zip dist/*.deb dist/*.rpm dist/checksums.txt; do
|
||||||
|
[ -f "$f" ] && ASSETS+=("$f")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create nightly release (prerelease, NOT latest)
|
||||||
gh release create nightly \
|
gh release create nightly \
|
||||||
--title "Nightly Build" \
|
--title "Nightly Build" \
|
||||||
--notes "$NOTES" \
|
--notes "$NOTES" \
|
||||||
--target "${{ github.sha }}" \
|
--target "${{ github.sha }}" \
|
||||||
--prerelease \
|
--prerelease \
|
||||||
build/*
|
--latest=false \
|
||||||
|
"${ASSETS[@]}"
|
||||||
|
|
||||||
echo "Cleaning up old nightly releases (keeping only the most recent)..."
|
|
||||||
gh release list --limit 100 --json tagName -q '.[].tagName | select(contains("-nightly."))' | tail -n +2 | while read -r old_tag; do
|
|
||||||
if [ -n "$old_tag" ] && [ "$old_tag" != "$TAG" ]; then
|
|
||||||
echo "Deleting old nightly release: $old_tag"
|
|
||||||
gh release delete "$old_tag" --cleanup-tag -y || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Cleaning up old 'vX.X.X-nightly...' Docker images on GHCR..."
|
|
||||||
OWNER="${{ github.repository_owner }}"
|
|
||||||
PACKAGE_NAME="${{ github.event.repository.name }}"
|
|
||||||
|
|
||||||
# Check if owner is an organization or user
|
|
||||||
ORG_TEST=$(gh api -H "Accept: application/vnd.github+json" /orgs/$OWNER 2>/dev/null || true)
|
|
||||||
if echo "$ORG_TEST" | grep -q '"login"'; then
|
|
||||||
ACCOUNT_TYPE="orgs"
|
|
||||||
else
|
|
||||||
ACCOUNT_TYPE="users"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PACKAGE_URL="/${ACCOUNT_TYPE}/${OWNER}/packages/container/${PACKAGE_NAME}/versions"
|
|
||||||
OLD_NIGHTLY_VERSIONS=$(gh api --paginate -H "Accept: application/vnd.github+json" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
"$PACKAGE_URL" \
|
|
||||||
--jq ". | map(select(any(.metadata.container.tags[]; contains(\"-nightly.\") and (. != \"nightly\") and (. != \"$TAG\")))) | .[].id" 2>/dev/null || true)
|
|
||||||
|
|
||||||
for version_id in $OLD_NIGHTLY_VERSIONS; do
|
|
||||||
if [ -n "$version_id" ]; then
|
|
||||||
echo "Deleting Docker image version ID: $version_id"
|
|
||||||
gh api -X DELETE -H "Accept: application/vnd.github+json" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
"/${ACCOUNT_TYPE}/${OWNER}/packages/container/${PACKAGE_NAME}/versions/$version_id" || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
|
||||||
|
|
@ -116,9 +116,9 @@ dockers_v2:
|
||||||
- picoclaw
|
- picoclaw
|
||||||
images:
|
images:
|
||||||
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
|
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
|
||||||
- '{{ if not (isEnvSet "NIGHTLY_BUILD") }}docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}{{ end }}'
|
- 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}'
|
||||||
tags:
|
tags:
|
||||||
- "{{ .Tag }}"
|
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}{{ .Tag }}{{ end }}'
|
||||||
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}latest{{ end }}'
|
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly{{ else }}latest{{ end }}'
|
||||||
platforms:
|
platforms:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
|
@ -133,9 +133,9 @@ dockers_v2:
|
||||||
- picoclaw-launcher-tui
|
- picoclaw-launcher-tui
|
||||||
images:
|
images:
|
||||||
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
|
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw"
|
||||||
- '{{ if not (isEnvSet "NIGHTLY_BUILD") }}docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}{{ end }}'
|
- 'docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}'
|
||||||
tags:
|
tags:
|
||||||
- "{{ .Tag }}-launcher"
|
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}{{ .Tag }}-launcher{{ end }}'
|
||||||
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}launcher{{ end }}'
|
- '{{ if isEnvSet "NIGHTLY_BUILD" }}nightly-launcher{{ else }}launcher{{ end }}'
|
||||||
platforms:
|
platforms:
|
||||||
- linux/amd64
|
- linux/amd64
|
||||||
|
|
@ -215,6 +215,7 @@ changelog:
|
||||||
# lzma: true
|
# lzma: true
|
||||||
|
|
||||||
release:
|
release:
|
||||||
|
disable: '{{ isEnvSet "NIGHTLY_BUILD" }}'
|
||||||
footer: >-
|
footer: >-
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
61
README.fr.md
61
README.fr.md
|
|
@ -1,5 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
<img src="assets/logo.webp" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
<h1>PicoClaw : Assistant IA Ultra-Efficace en Go</h1>
|
<h1>PicoClaw : Assistant IA Ultra-Efficace en Go</h1>
|
||||||
|
|
||||||
|
|
@ -206,9 +206,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
|
||||||
### 🚀 Démarrage Rapide
|
### 🚀 Démarrage Rapide
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Configurez votre clé API dans `~/.picoclaw/config.json`.
|
> Configurez votre clé API dans `~/.picoclaw/config.json`. Obtenez des clés API : [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). La recherche web est optionnelle — obtenez gratuitement l'[API Tavily](https://tavily.com) (1000 requêtes gratuites/mois) ou l'[API Brave Search](https://brave.com/search/api) (2000 requêtes gratuites/mois).
|
||||||
> Obtenir des clés API : [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
|
||||||
> La recherche web est **optionnelle** — obtenez gratuitement l'[API Brave Search](https://brave.com/search/api) (2000 requêtes gratuites/mois) ou utilisez le repli automatique intégré.
|
|
||||||
|
|
||||||
**1. Initialiser**
|
**1. Initialiser**
|
||||||
|
|
||||||
|
|
@ -222,8 +220,13 @@ picoclaw onboard
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key",
|
"api_key": "sk-your-openai-key",
|
||||||
"request_timeout": 300,
|
"request_timeout": 300,
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
|
|
@ -231,7 +234,7 @@ picoclaw onboard
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model_name": "gpt4"
|
"model_name": "gpt-5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
|
|
@ -977,8 +980,9 @@ Cette conception permet également le **support multi-agent** avec une sélectio
|
||||||
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obtenir Clé](https://openrouter.ai/keys) |
|
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obtenir Clé](https://openrouter.ai/keys) |
|
||||||
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
||||||
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obtenir Clé](https://cerebras.ai) |
|
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obtenir Clé](https://cerebras.ai) |
|
||||||
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obtenir Clé](https://console.volcengine.com) |
|
| **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obtenir Clé](https://console.volcengine.com) |
|
||||||
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
||||||
|
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Obtenir Clé](https://console.volcengine.com) |
|
||||||
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Obtenir une clé](https://longcat.chat/platform) |
|
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Obtenir une clé](https://longcat.chat/platform) |
|
||||||
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Obtenir Clé](https://portal.azure.com) |
|
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Obtenir Clé](https://portal.azure.com) |
|
||||||
| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth uniquement |
|
| **Antigravity** | `antigravity/` | Google Cloud | Custom | OAuth uniquement |
|
||||||
|
|
@ -990,8 +994,13 @@ Cette conception permet également le **support multi-agent** avec une sélectio
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1007,7 +1016,7 @@ Cette conception permet également le **support multi-agent** avec une sélectio
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt-5.2"
|
"model": "gpt-5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1018,8 +1027,17 @@ Cette conception permet également le **support multi-agent** avec une sélectio
|
||||||
**OpenAI**
|
**OpenAI**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
|
"api_key": "sk-..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**VolcEngine (Doubao)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_name": "ark-code-latest",
|
||||||
|
"model": "volcengine/ark-code-latest",
|
||||||
"api_key": "sk-..."
|
"api_key": "sk-..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -1062,14 +1080,14 @@ Configurez plusieurs points de terminaison pour le même nom de modèle—PicoCl
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api1.example.com/v1",
|
"api_base": "https://api1.example.com/v1",
|
||||||
"api_key": "sk-key1"
|
"api_key": "sk-key1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api2.example.com/v1",
|
"api_base": "https://api2.example.com/v1",
|
||||||
"api_key": "sk-key2"
|
"api_key": "sk-key2"
|
||||||
}
|
}
|
||||||
|
|
@ -1201,6 +1219,13 @@ Cela se produit lorsqu'une autre instance du bot est en cours d'exécution. Assu
|
||||||
| Service | Offre Gratuite | Cas d'Utilisation |
|
| Service | Offre Gratuite | Cas d'Utilisation |
|
||||||
| ---------------- | -------------------- | ------------------------------------- |
|
| ---------------- | -------------------- | ------------------------------------- |
|
||||||
| **OpenRouter** | 200K tokens/mois | Multiples modèles (Claude, GPT-4, etc.) |
|
| **OpenRouter** | 200K tokens/mois | Multiples modèles (Claude, GPT-4, etc.) |
|
||||||
| **Zhipu** | 200K tokens/mois | Idéal pour les utilisateurs chinois |
|
| **Volcengine CodingPlan** | 9,9¥/premier mois | Idéal pour les utilisateurs chinois, multiples modèles SOTA (Doubao, DeepSeek, etc.) |
|
||||||
|
| **Zhipu** | 200K tokens/mois | Convient aux utilisateurs chinois |
|
||||||
| **Brave Search** | 2000 requêtes/mois | Fonctionnalité de recherche web |
|
| **Brave Search** | 2000 requêtes/mois | Fonctionnalité de recherche web |
|
||||||
| **Groq** | Offre gratuite dispo | Inférence ultra-rapide (Llama, Mixtral) |
|
| **Groq** | Offre gratuite dispo | Inférence ultra-rapide (Llama, Mixtral) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
|
||||||
|
</div>
|
||||||
|
|
|
||||||
61
README.ja.md
61
README.ja.md
|
|
@ -1,5 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
<img src="assets/logo.webp" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
|
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
|
||||||
|
|
||||||
|
|
@ -168,9 +168,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
|
||||||
### 🚀 クイックスタート(ネイティブ)
|
### 🚀 クイックスタート(ネイティブ)
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> `~/.picoclaw/config.json` に API キーを設定してください。
|
> `~/.picoclaw/config.json` に API キーを設定してください。API キーの取得先: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)。Web 検索は **任意** です — 無料の [Tavily API](https://tavily.com) (月 1000 クエリ無料) または [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料)。
|
||||||
> API キーの取得先: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
|
||||||
> Web 検索は **任意** です - 無料の [Tavily API](https://tavily.com) (月 1000 クエリ無料) または [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料)
|
|
||||||
|
|
||||||
**1. 初期化**
|
**1. 初期化**
|
||||||
|
|
||||||
|
|
@ -184,8 +182,13 @@ picoclaw onboard
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key",
|
"api_key": "sk-your-openai-key",
|
||||||
"request_timeout": 300,
|
"request_timeout": 300,
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
|
|
@ -193,7 +196,7 @@ picoclaw onboard
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model_name": "gpt4"
|
"model_name": "gpt-5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"channels": {
|
"channels": {
|
||||||
|
|
@ -918,8 +921,9 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
|
||||||
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [キーを取得](https://openrouter.ai/keys) |
|
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [キーを取得](https://openrouter.ai/keys) |
|
||||||
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | ローカル |
|
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | ローカル |
|
||||||
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [キーを取得](https://cerebras.ai) |
|
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [キーを取得](https://cerebras.ai) |
|
||||||
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [キーを取得](https://console.volcengine.com) |
|
| **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [キーを取得](https://console.volcengine.com) |
|
||||||
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
||||||
|
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [キーを取得](https://console.volcengine.com) |
|
||||||
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [キーを取得](https://longcat.chat/platform) |
|
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [キーを取得](https://longcat.chat/platform) |
|
||||||
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [キーを取得](https://portal.azure.com) |
|
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [キーを取得](https://portal.azure.com) |
|
||||||
| **Antigravity** | `antigravity/` | Google Cloud | カスタム | OAuthのみ |
|
| **Antigravity** | `antigravity/` | Google Cloud | カスタム | OAuthのみ |
|
||||||
|
|
@ -931,8 +935,13 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -948,7 +957,7 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt-5.2"
|
"model": "gpt-5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -959,8 +968,17 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
|
||||||
**OpenAI**
|
**OpenAI**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
|
"api_key": "sk-..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**VolcEngine (Doubao)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_name": "ark-code-latest",
|
||||||
|
"model": "volcengine/ark-code-latest",
|
||||||
"api_key": "sk-..."
|
"api_key": "sk-..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -1003,14 +1021,14 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api1.example.com/v1",
|
"api_base": "https://api1.example.com/v1",
|
||||||
"api_key": "sk-key1"
|
"api_key": "sk-key1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api2.example.com/v1",
|
"api_base": "https://api2.example.com/v1",
|
||||||
"api_key": "sk-key2"
|
"api_key": "sk-key2"
|
||||||
}
|
}
|
||||||
|
|
@ -1121,9 +1139,16 @@ Web 検索を有効にするには:
|
||||||
| サービス | 無料枠 | ユースケース |
|
| サービス | 無料枠 | ユースケース |
|
||||||
|---------|--------|------------|
|
|---------|--------|------------|
|
||||||
| **OpenRouter** | 月 200K トークン | 複数モデル(Claude, GPT-4 など) |
|
| **OpenRouter** | 月 200K トークン | 複数モデル(Claude, GPT-4 など) |
|
||||||
| **Zhipu** | 月 200K トークン | 中国ユーザー向け最適 |
|
| **Volcengine CodingPlan** | 9.9元/初月 | 中国ユーザーに最適、複数のSOTAモデル(Doubao、DeepSeek等) |
|
||||||
|
| **Zhipu** | 月 200K トークン | 中国ユーザーに適している |
|
||||||
| **Qwen** | 無料枠あり | 通義千問 (Qwen) |
|
| **Qwen** | 無料枠あり | 通義千問 (Qwen) |
|
||||||
| **Brave Search** | 月 2000 クエリ | Web 検索機能 |
|
| **Brave Search** | 月 2000 クエリ | Web 検索機能 |
|
||||||
| **Tavily** | 月 1000 クエリ | AI エージェント検索最適化 |
|
| **Tavily** | 月 1000 クエリ | AI エージェント検索最適化 |
|
||||||
| **Groq** | 無料枠あり | 高速推論(Llama, Mixtral) |
|
| **Groq** | 無料枠あり | 高速推論(Llama, Mixtral) |
|
||||||
| **Cerebras** | 無料枠あり | 高速推論(Llama, Qwen など) |
|
| **Cerebras** | 無料枠あり | 高速推論(Llama, Qwen など) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
|
||||||
|
</div>
|
||||||
|
|
|
||||||
62
README.md
62
README.md
|
|
@ -1,5 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
<img src="assets/logo.webp" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
<h1>PicoClaw: Ultra-Efficient AI Assistant in Go</h1>
|
<h1>PicoClaw: Ultra-Efficient AI Assistant in Go</h1>
|
||||||
|
|
||||||
|
|
@ -227,9 +227,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
|
||||||
### 🚀 Quick Start
|
### 🚀 Quick Start
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Set your API key in `~/.picoclaw/config.json`.
|
> Set your API Key in `~/.picoclaw/config.json`. Get API Keys: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Web search is optional — get a free [Tavily API](https://tavily.com) (1000 free queries/month) or [Brave Search API](https://brave.com/search/api) (2000 free queries/month).
|
||||||
> Get API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
|
||||||
> Web Search is **optional** - get free [Tavily API](https://tavily.com) (1000 free queries/month), [SearXNG](https://github.com/searxng/searxng) (free, self-hosted) or [Brave Search API](https://brave.com/search/api) (2000 free queries/month) or use built-in auto fallback.
|
|
||||||
|
|
||||||
**1. Initialize**
|
**1. Initialize**
|
||||||
|
|
||||||
|
|
@ -244,7 +242,7 @@ picoclaw onboard
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"workspace": "~/.picoclaw/workspace",
|
"workspace": "~/.picoclaw/workspace",
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt-5.4",
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tool_iterations": 20
|
"max_tool_iterations": 20
|
||||||
|
|
@ -252,8 +250,13 @@ picoclaw onboard
|
||||||
},
|
},
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "your-api-key",
|
"api_key": "your-api-key",
|
||||||
"request_timeout": 300
|
"request_timeout": 300
|
||||||
},
|
},
|
||||||
|
|
@ -1031,8 +1034,9 @@ This design also enables **multi-agent support** with flexible provider selectio
|
||||||
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key |
|
| **LiteLLM Proxy** | `litellm/` | `http://localhost:4000/v1` | OpenAI | Your LiteLLM proxy key |
|
||||||
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
||||||
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
|
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Get Key](https://cerebras.ai) |
|
||||||
| **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) |
|
| **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) |
|
||||||
| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
||||||
|
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Get Key](https://console.volcengine.com) |
|
||||||
| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) |
|
| **Vivgrid** | `vivgrid/` | `https://api.vivgrid.com/v1` | OpenAI | [Get Key](https://vivgrid.com) |
|
||||||
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Get Key](https://longcat.chat/platform) |
|
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Get Key](https://longcat.chat/platform) |
|
||||||
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Get Key](https://portal.azure.com) |
|
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Get Key](https://portal.azure.com) |
|
||||||
|
|
@ -1045,8 +1049,13 @@ This design also enables **multi-agent support** with flexible provider selectio
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1062,7 +1071,7 @@ This design also enables **multi-agent support** with flexible provider selectio
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt-5.2"
|
"model": "gpt-5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1074,8 +1083,18 @@ This design also enables **multi-agent support** with flexible provider selectio
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
|
"api_key": "sk-..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**VolcEngine (Doubao)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_name": "ark-code-latest",
|
||||||
|
"model": "volcengine/ark-code-latest",
|
||||||
"api_key": "sk-..."
|
"api_key": "sk-..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -1154,14 +1173,14 @@ Configure multiple endpoints for the same model name—PicoClaw will automatical
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api1.example.com/v1",
|
"api_base": "https://api1.example.com/v1",
|
||||||
"api_key": "sk-key1"
|
"api_key": "sk-key1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api2.example.com/v1",
|
"api_base": "https://api2.example.com/v1",
|
||||||
"api_key": "sk-key2"
|
"api_key": "sk-key2"
|
||||||
}
|
}
|
||||||
|
|
@ -1501,9 +1520,16 @@ This happens when another instance of the bot is running. Make sure only one `pi
|
||||||
| Service | Free Tier | Use Case |
|
| Service | Free Tier | Use Case |
|
||||||
| ---------------- | ------------------------ | ------------------------------------- |
|
| ---------------- | ------------------------ | ------------------------------------- |
|
||||||
| **OpenRouter** | 200K tokens/month | Multiple models (Claude, GPT-4, etc.) |
|
| **OpenRouter** | 200K tokens/month | Multiple models (Claude, GPT-4, etc.) |
|
||||||
| **Zhipu** | 200K tokens/month | Best for Chinese users |
|
| **Volcengine CodingPlan** | ¥9.9/first month | Best for Chinese users, multiple SOTA models (Doubao, DeepSeek, etc.) |
|
||||||
|
| **Zhipu** | 200K tokens/month | Suitable for Chinese users |
|
||||||
| **Brave Search** | Paid ($5/1000 queries) | Web search functionality |
|
| **Brave Search** | Paid ($5/1000 queries) | Web search functionality |
|
||||||
| **SearXNG** | Unlimited (self-hosted) | Privacy-focused metasearch (70+ engines) |
|
| **SearXNG** | Unlimited (self-hosted) | Privacy-focused metasearch (70+ engines) |
|
||||||
| **Groq** | Free tier available | Fast inference (Llama, Mixtral) |
|
| **Groq** | Free tier available | Fast inference (Llama, Mixtral) |
|
||||||
| **Cerebras** | Free tier available | Fast inference (Llama, Qwen, etc.) |
|
| **Cerebras** | Free tier available | Fast inference (Llama, Qwen, etc.) |
|
||||||
| **LongCat** | Up to 5M tokens/day | Fast inference (free tier) |
|
| **LongCat** | Up to 5M tokens/day | Fast inference (free tier) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
<img src="assets/logo.webp" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
<h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1>
|
<h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1>
|
||||||
|
|
||||||
|
|
@ -207,9 +207,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
|
||||||
### 🚀 Início Rápido
|
### 🚀 Início Rápido
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Configure sua API key em `~/.picoclaw/config.json`.
|
> Configure sua API key em `~/.picoclaw/config.json`. Obtenha API keys: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Busca web é **opcional** — obtenha a [API Tavily](https://tavily.com) gratuita (1000 consultas grátis/mês) ou a [Brave Search API](https://brave.com/search/api) (2000 consultas grátis/mês).
|
||||||
> Obtenha API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
|
||||||
> Busca web e **opcional** — obtenha a [Brave Search API](https://brave.com/search/api) gratuita (2000 consultas grátis/mês) ou use o fallback automático integrado.
|
|
||||||
|
|
||||||
**1. Inicializar**
|
**1. Inicializar**
|
||||||
|
|
||||||
|
|
@ -223,8 +221,13 @@ picoclaw onboard
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key",
|
"api_key": "sk-your-openai-key",
|
||||||
"request_timeout": 300,
|
"request_timeout": 300,
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
|
|
@ -232,7 +235,7 @@ picoclaw onboard
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model_name": "gpt4"
|
"model_name": "gpt-5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
|
|
@ -973,8 +976,9 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
|
||||||
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obter Chave](https://openrouter.ai/keys) |
|
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Obter Chave](https://openrouter.ai/keys) |
|
||||||
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
||||||
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obter Chave](https://cerebras.ai) |
|
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Obter Chave](https://cerebras.ai) |
|
||||||
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obter Chave](https://console.volcengine.com) |
|
| **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Obter Chave](https://console.volcengine.com) |
|
||||||
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
||||||
|
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Obter Chave](https://console.volcengine.com) |
|
||||||
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Obter Chave](https://longcat.chat/platform) |
|
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Obter Chave](https://longcat.chat/platform) |
|
||||||
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Obter Chave](https://portal.azure.com) |
|
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Obter Chave](https://portal.azure.com) |
|
||||||
| **Antigravity** | `antigravity/` | Google Cloud | Custom | Apenas OAuth |
|
| **Antigravity** | `antigravity/` | Google Cloud | Custom | Apenas OAuth |
|
||||||
|
|
@ -986,8 +990,13 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1003,7 +1012,7 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt-5.2"
|
"model": "gpt-5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1014,8 +1023,17 @@ Este design também possibilita o **suporte multi-agent** com seleção flexíve
|
||||||
**OpenAI**
|
**OpenAI**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
|
"api_key": "sk-..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**VolcEngine (Doubao)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_name": "ark-code-latest",
|
||||||
|
"model": "volcengine/ark-code-latest",
|
||||||
"api_key": "sk-..."
|
"api_key": "sk-..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -1058,14 +1076,14 @@ Configure vários endpoints para o mesmo nome de modelo—PicoClaw fará round-r
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api1.example.com/v1",
|
"api_base": "https://api1.example.com/v1",
|
||||||
"api_key": "sk-key1"
|
"api_key": "sk-key1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api2.example.com/v1",
|
"api_base": "https://api2.example.com/v1",
|
||||||
"api_key": "sk-key2"
|
"api_key": "sk-key2"
|
||||||
}
|
}
|
||||||
|
|
@ -1197,7 +1215,14 @@ Isso acontece quando outra instância do bot está em execução. Certifique-se
|
||||||
| Serviço | Plano Gratuito | Caso de Uso |
|
| Serviço | Plano Gratuito | Caso de Uso |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **OpenRouter** | 200K tokens/mês | Múltiplos modelos (Claude, GPT-4, etc.) |
|
| **OpenRouter** | 200K tokens/mês | Múltiplos modelos (Claude, GPT-4, etc.) |
|
||||||
| **Zhipu** | 200K tokens/mês | Melhor para usuários chineses |
|
| **Volcengine CodingPlan** | ¥9,9/primeiro mês | Ideal para usuários chineses, múltiplos modelos SOTA (Doubao, DeepSeek, etc.) |
|
||||||
|
| **Zhipu** | 200K tokens/mês | Adequado para usuários chineses |
|
||||||
| **Brave Search** | 2000 consultas/mês | Funcionalidade de busca web |
|
| **Brave Search** | 2000 consultas/mês | Funcionalidade de busca web |
|
||||||
| **Groq** | Plano gratuito disponível | Inferência ultra-rápida (Llama, Mixtral) |
|
| **Groq** | Plano gratuito disponível | Inferência ultra-rápida (Llama, Mixtral) |
|
||||||
| **Cerebras** | Plano gratuito disponível | Inferência ultra-rápida (Llama 3.3 70B) |
|
| **Cerebras** | Plano gratuito disponível | Inferência ultra-rápida (Llama 3.3 70B) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
|
||||||
|
</div>
|
||||||
|
|
|
||||||
59
README.vi.md
59
README.vi.md
|
|
@ -1,5 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
<img src="assets/logo.webp" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
<h1>PicoClaw: Trợ lý AI Siêu Nhẹ viết bằng Go</h1>
|
<h1>PicoClaw: Trợ lý AI Siêu Nhẹ viết bằng Go</h1>
|
||||||
|
|
||||||
|
|
@ -187,9 +187,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
|
||||||
### 🚀 Bắt đầu nhanh
|
### 🚀 Bắt đầu nhanh
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Thiết lập API key trong `~/.picoclaw/config.json`.
|
> Thiết lập API key trong `~/.picoclaw/config.json`. Lấy API key: [Volcengine (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM). Tìm kiếm web là **tùy chọn** — lấy [Tavily API](https://tavily.com) miễn phí (1000 truy vấn/tháng) hoặc [Brave Search API](https://brave.com/search/api) (2000 truy vấn/tháng).
|
||||||
> Lấy API key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
|
||||||
> Tìm kiếm web là **tùy chọn** — lấy [Brave Search API](https://brave.com/search/api) miễn phí (2000 truy vấn/tháng) hoặc dùng tính năng auto fallback tích hợp sẵn.
|
|
||||||
|
|
||||||
**1. Khởi tạo**
|
**1. Khởi tạo**
|
||||||
|
|
||||||
|
|
@ -203,8 +201,13 @@ picoclaw onboard
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key",
|
"api_key": "sk-your-openai-key",
|
||||||
"request_timeout": 300,
|
"request_timeout": 300,
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
|
|
@ -942,8 +945,9 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
|
||||||
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Lấy Khóa](https://openrouter.ai/keys) |
|
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [Lấy Khóa](https://openrouter.ai/keys) |
|
||||||
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | Local |
|
||||||
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Lấy Khóa](https://cerebras.ai) |
|
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [Lấy Khóa](https://cerebras.ai) |
|
||||||
| **Volcengine** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Lấy Khóa](https://console.volcengine.com) |
|
| **VolcEngine (Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [Lấy Khóa](https://console.volcengine.com) |
|
||||||
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
| **ShengsuanYun** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
||||||
|
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [Lấy Khóa](https://console.volcengine.com) |
|
||||||
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Lấy Key](https://longcat.chat/platform) |
|
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [Lấy Key](https://longcat.chat/platform) |
|
||||||
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Lấy Khóa](https://portal.azure.com) |
|
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [Lấy Khóa](https://portal.azure.com) |
|
||||||
| **Antigravity** | `antigravity/` | Google Cloud | Tùy chỉnh | Chỉ OAuth |
|
| **Antigravity** | `antigravity/` | Google Cloud | Tùy chỉnh | Chỉ OAuth |
|
||||||
|
|
@ -955,8 +959,13 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -972,7 +981,7 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt-5.2"
|
"model": "gpt-5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -983,8 +992,17 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
|
||||||
**OpenAI**
|
**OpenAI**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
|
"api_key": "sk-..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**VolcEngine (Doubao)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_name": "ark-code-latest",
|
||||||
|
"model": "volcengine/ark-code-latest",
|
||||||
"api_key": "sk-..."
|
"api_key": "sk-..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -1027,14 +1045,14 @@ Thiết kế này cũng cho phép **hỗ trợ đa tác nhân** với lựa ch
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api1.example.com/v1",
|
"api_base": "https://api1.example.com/v1",
|
||||||
"api_key": "sk-key1"
|
"api_key": "sk-key1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api2.example.com/v1",
|
"api_base": "https://api2.example.com/v1",
|
||||||
"api_key": "sk-key2"
|
"api_key": "sk-key2"
|
||||||
}
|
}
|
||||||
|
|
@ -1166,6 +1184,13 @@ Một số nhà cung cấp (như Zhipu) có bộ lọc nội dung nghiêm ngặt
|
||||||
| Dịch vụ | Gói miễn phí | Trường hợp sử dụng |
|
| Dịch vụ | Gói miễn phí | Trường hợp sử dụng |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **OpenRouter** | 200K tokens/tháng | Đa model (Claude, GPT-4, v.v.) |
|
| **OpenRouter** | 200K tokens/tháng | Đa model (Claude, GPT-4, v.v.) |
|
||||||
| **Zhipu** | 200K tokens/tháng | Tốt nhất cho người dùng Trung Quốc |
|
| **Volcengine CodingPlan** | ¥9.9/tháng đầu | Tốt nhất cho người dùng Trung Quốc, nhiều mô hình SOTA (Doubao, DeepSeek, v.v.) |
|
||||||
|
| **Zhipu** | 200K tokens/tháng | Phù hợp cho người dùng Trung Quốc |
|
||||||
| **Brave Search** | 2000 truy vấn/tháng | Chức năng tìm kiếm web |
|
| **Brave Search** | 2000 truy vấn/tháng | Chức năng tìm kiếm web |
|
||||||
| **Groq** | Có gói miễn phí | Suy luận siêu nhanh (Llama, Mixtral) |
|
| **Groq** | Có gói miễn phí | Suy luận siêu nhanh (Llama, Mixtral) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
|
||||||
|
</div>
|
||||||
|
|
|
||||||
62
README.zh.md
62
README.zh.md
|
|
@ -1,5 +1,5 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
|
<img src="assets/logo.webp" alt="PicoClaw" width="512">
|
||||||
|
|
||||||
<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>
|
<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>
|
||||||
|
|
||||||
|
|
@ -208,9 +208,7 @@ docker compose -f docker/docker-compose.yml --profile gateway up -d
|
||||||
### 🚀 快速开始
|
### 🚀 快速开始
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> 在 `~/.picoclaw/config.json` 中设置您的 API Key。
|
> 在 `~/.picoclaw/config.json` 中设置您的 API Key。获取 API Key: [火山引擎 (CodingPlan)](https://console.volcengine.com) (LLM) · [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)。网络搜索是 **可选的** — 获取免费的 [Tavily API](https://tavily.com) (每月 1000 次免费查询) 或 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)。
|
||||||
> 获取 API Key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
|
|
||||||
> 网络搜索是 **可选的** - 获取免费的 [Tavily API](https://tavily.com) (每月 1000 次免费查询) 或 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)
|
|
||||||
|
|
||||||
**1. 初始化 (Initialize)**
|
**1. 初始化 (Initialize)**
|
||||||
|
|
||||||
|
|
@ -226,7 +224,7 @@ picoclaw onboard
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"workspace": "~/.picoclaw/workspace",
|
"workspace": "~/.picoclaw/workspace",
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt-5.4",
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tool_iterations": 20
|
"max_tool_iterations": 20
|
||||||
|
|
@ -234,8 +232,13 @@ picoclaw onboard
|
||||||
},
|
},
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "your-api-key",
|
"api_key": "your-api-key",
|
||||||
"request_timeout": 300
|
"request_timeout": 300
|
||||||
},
|
},
|
||||||
|
|
@ -514,8 +517,9 @@ Agent 读取 HEARTBEAT.md
|
||||||
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
|
| **OpenRouter** | `openrouter/` | `https://openrouter.ai/api/v1` | OpenAI | [获取密钥](https://openrouter.ai/keys) |
|
||||||
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 |
|
| **VLLM** | `vllm/` | `http://localhost:8000/v1` | OpenAI | 本地 |
|
||||||
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) |
|
| **Cerebras** | `cerebras/` | `https://api.cerebras.ai/v1` | OpenAI | [获取密钥](https://cerebras.ai) |
|
||||||
| **火山引擎** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) |
|
| **火山引擎(Doubao)** | `volcengine/` | `https://ark.cn-beijing.volces.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) |
|
||||||
| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
| **神算云** | `shengsuanyun/` | `https://router.shengsuanyun.com/api/v1` | OpenAI | - |
|
||||||
|
| **BytePlus** | `byteplus/` | `https://ark.ap-southeast.bytepluses.com/api/v3` | OpenAI | [获取密钥](https://console.volcengine.com) |
|
||||||
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [获取密钥](https://longcat.chat/platform) |
|
| **LongCat** | `longcat/` | `https://api.longcat.chat/openai` | OpenAI | [获取密钥](https://longcat.chat/platform) |
|
||||||
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [获取密钥](https://portal.azure.com) |
|
| **Azure OpenAI** | `azure/` | `https://{resource}.openai.azure.com` | Azure | [获取密钥](https://portal.azure.com) |
|
||||||
| **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth |
|
| **Antigravity** | `antigravity/` | Google Cloud | 自定义 | 仅 OAuth |
|
||||||
|
|
@ -527,8 +531,13 @@ Agent 读取 HEARTBEAT.md
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "ark-code-latest",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "volcengine/ark-code-latest",
|
||||||
|
"api_key": "sk-your-api-key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model_name": "gpt-5.4",
|
||||||
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key"
|
"api_key": "sk-your-openai-key"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -544,7 +553,7 @@ Agent 读取 HEARTBEAT.md
|
||||||
],
|
],
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"model": "gpt-5.2"
|
"model": "gpt-5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -556,8 +565,18 @@ Agent 读取 HEARTBEAT.md
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
|
"api_key": "sk-..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**火山引擎(Doubao)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"model_name": "ark-code-latest",
|
||||||
|
"model": "volcengine/ark-code-latest",
|
||||||
"api_key": "sk-..."
|
"api_key": "sk-..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
@ -623,14 +642,14 @@ Agent 读取 HEARTBEAT.md
|
||||||
{
|
{
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api1.example.com/v1",
|
"api_base": "https://api1.example.com/v1",
|
||||||
"api_key": "sk-key1"
|
"api_key": "sk-key1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_base": "https://api2.example.com/v1",
|
"api_base": "https://api2.example.com/v1",
|
||||||
"api_key": "sk-key2"
|
"api_key": "sk-key2"
|
||||||
}
|
}
|
||||||
|
|
@ -876,8 +895,15 @@ Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
|
||||||
| 服务 | 免费层级 | 适用场景 |
|
| 服务 | 免费层级 | 适用场景 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
|
| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
|
||||||
| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
|
| **火山引擎 CodingPlan** | 9.9 元/首月 | 最适合国内用户,多种 SOTA 模型(豆包、DeepSeek 等) |
|
||||||
|
| **智谱 (Zhipu)** | 200K tokens/月 | 适合中国用户 |
|
||||||
| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
|
| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
|
||||||
| **Tavily** | 1000 次查询/月 | AI Agent 搜索优化 |
|
| **Tavily** | 1000 次查询/月 | AI Agent 搜索优化 |
|
||||||
| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
|
| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |
|
||||||
| **LongCat** | 最多 5M tokens/天 | 推理速度快 (免费额度) |
|
| **LongCat** | 最多 5M tokens/天 | 推理速度快 (免费额度) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/logo.jpg" alt="PicoClaw Meme" width="512">
|
||||||
|
</div>
|
||||||
|
|
|
||||||
BIN
assets/logo.webp
Normal file
BIN
assets/logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
|
|
@ -49,7 +49,7 @@ func (s *appState) modelMenu() tview.Primitive {
|
||||||
Action: func() {
|
Action: func() {
|
||||||
newName := s.nextAvailableModelName("new-model")
|
newName := s.nextAvailableModelName("new-model")
|
||||||
s.addModel(
|
s.addModel(
|
||||||
picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.2"},
|
picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.4"},
|
||||||
)
|
)
|
||||||
s.push(
|
s.push(
|
||||||
fmt.Sprintf("model-%d", len(s.config.ModelList)-1),
|
fmt.Sprintf("model-%d", len(s.config.ModelList)-1),
|
||||||
|
|
@ -291,7 +291,7 @@ func refreshModelMenuFromState(menu *Menu, s *appState) {
|
||||||
Action: func() {
|
Action: func() {
|
||||||
newName := s.nextAvailableModelName("new-model")
|
newName := s.nextAvailableModelName("new-model")
|
||||||
s.addModel(
|
s.addModel(
|
||||||
picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.2"},
|
picoclawconfig.ModelConfig{ModelName: newName, Model: "openai/gpt-5.4"},
|
||||||
)
|
)
|
||||||
s.push(fmt.Sprintf("model-%d", len(s.config.ModelList)-1), s.modelForm(len(s.config.ModelList)-1))
|
s.push(fmt.Sprintf("model-%d", len(s.config.ModelList)-1), s.modelForm(len(s.config.ModelList)-1))
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -72,14 +72,14 @@ func authLoginOpenAI(useDeviceCode bool) error {
|
||||||
// If no openai in ModelList, add it
|
// If no openai in ModelList, add it
|
||||||
if !foundOpenAI {
|
if !foundOpenAI {
|
||||||
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
|
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
|
||||||
ModelName: "gpt-5.2",
|
ModelName: "gpt-5.4",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
AuthMethod: "oauth",
|
AuthMethod: "oauth",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update default model to use OpenAI
|
// Update default model to use OpenAI
|
||||||
appCfg.Agents.Defaults.ModelName = "gpt-5.2"
|
appCfg.Agents.Defaults.ModelName = "gpt-5.4"
|
||||||
|
|
||||||
if err = config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
|
if err = config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
|
||||||
return fmt.Errorf("could not update config: %w", err)
|
return fmt.Errorf("could not update config: %w", err)
|
||||||
|
|
@ -90,7 +90,7 @@ func authLoginOpenAI(useDeviceCode bool) error {
|
||||||
if cred.AccountID != "" {
|
if cred.AccountID != "" {
|
||||||
fmt.Printf("Account: %s\n", cred.AccountID)
|
fmt.Printf("Account: %s\n", cred.AccountID)
|
||||||
}
|
}
|
||||||
fmt.Println("Default model set to: gpt-5.2")
|
fmt.Println("Default model set to: gpt-5.4")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -318,13 +318,13 @@ func authLoginPasteToken(provider string) error {
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
|
appCfg.ModelList = append(appCfg.ModelList, config.ModelConfig{
|
||||||
ModelName: "gpt-5.2",
|
ModelName: "gpt-5.4",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
AuthMethod: "token",
|
AuthMethod: "token",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Update default model
|
// Update default model
|
||||||
appCfg.Agents.Defaults.ModelName = "gpt-5.2"
|
appCfg.Agents.Defaults.ModelName = "gpt-5.4"
|
||||||
}
|
}
|
||||||
if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
|
if err := config.SaveConfig(internal.GetConfigPath(), appCfg); err != nil {
|
||||||
return fmt.Errorf("could not update config: %w", err)
|
return fmt.Errorf("could not update config: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"workspace": "~/.picoclaw/workspace",
|
"workspace": "~/.picoclaw/workspace",
|
||||||
"restrict_to_workspace": true,
|
"restrict_to_workspace": true,
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt-5.4",
|
||||||
"max_tokens": 8192,
|
"max_tokens": 8192,
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tool_iterations": 20,
|
"max_tool_iterations": 20,
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
},
|
},
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key",
|
"api_key": "sk-your-openai-key",
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
},
|
},
|
||||||
|
|
@ -47,14 +47,14 @@
|
||||||
"api_base": "https://your-resource.openai.azure.com"
|
"api_base": "https://your-resource.openai.azure.com"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "loadbalanced-gpt4",
|
"model_name": "loadbalanced-gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-key1",
|
"api_key": "sk-key1",
|
||||||
"api_base": "https://api1.example.com/v1"
|
"api_base": "https://api1.example.com/v1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "loadbalanced-gpt4",
|
"model_name": "loadbalanced-gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-key2",
|
"api_key": "sk-key2",
|
||||||
"api_base": "https://api2.example.com/v1"
|
"api_base": "https://api2.example.com/v1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ Problem: Agent needs to know both `provider` and `model`, adding complexity.
|
||||||
Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
|
Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
|
||||||
|
|
||||||
1. **Model-centric**: Users care about models, not providers
|
1. **Model-centric**: Users care about models, not providers
|
||||||
2. **Protocol prefix**: Use `protocol/model_name` format, e.g., `openai/gpt-5.2`, `anthropic/claude-sonnet-4.6`
|
2. **Protocol prefix**: Use `protocol/model_name` format, e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4.6`
|
||||||
3. **Configuration-driven**: Adding new Providers only requires config changes, no code changes
|
3. **Configuration-driven**: Adding new Providers only requires config changes, no code changes
|
||||||
|
|
||||||
### 2.2 New Configuration Structure
|
### 2.2 New Configuration Structure
|
||||||
|
|
@ -81,8 +81,8 @@ Inspired by [LiteLLM](https://docs.litellm.ai/docs/proxy/configs) design:
|
||||||
"api_key": "sk-xxx"
|
"api_key": "sk-xxx"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt-5.2",
|
"model_name": "gpt-5.4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-xxx"
|
"api_key": "sk-xxx"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -128,7 +128,7 @@ type Config struct {
|
||||||
type ModelConfig struct {
|
type ModelConfig struct {
|
||||||
// Required
|
// Required
|
||||||
ModelName string `json:"model_name"` // user-facing name (alias)
|
ModelName string `json:"model_name"` // user-facing name (alias)
|
||||||
Model string `json:"model"` // protocol/model, e.g., openai/gpt-5.2
|
Model string `json:"model"` // protocol/model, e.g., openai/gpt-5.4
|
||||||
|
|
||||||
// Common config
|
// Common config
|
||||||
APIBase string `json:"api_base,omitempty"`
|
APIBase string `json:"api_base,omitempty"`
|
||||||
|
|
@ -180,7 +180,7 @@ Identify protocol via prefix in `model` field:
|
||||||
"model": "deepseek-chat"
|
"model": "deepseek-chat"
|
||||||
},
|
},
|
||||||
"coder": {
|
"coder": {
|
||||||
"model": "gpt-5.2",
|
"model": "gpt-5.4",
|
||||||
"system_prompt": "You are a coding assistant..."
|
"system_prompt": "You are a coding assistant..."
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
|
|
@ -200,7 +200,7 @@ Each Agent only needs to specify `model` (corresponds to `model_name` in `model_
|
||||||
model_list:
|
model_list:
|
||||||
- model_name: gpt-4o
|
- model_name: gpt-4o
|
||||||
litellm_params:
|
litellm_params:
|
||||||
model: openai/gpt-5.2
|
model: openai/gpt-5.4
|
||||||
api_key: xxx
|
api_key: xxx
|
||||||
- model_name: my-custom
|
- model_name: my-custom
|
||||||
litellm_params:
|
litellm_params:
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ The new `model_list` configuration offers several advantages:
|
||||||
"agents": {
|
"agents": {
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"model": "gpt-5.2"
|
"model": "gpt-5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ The new `model_list` configuration offers several advantages:
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-your-openai-key",
|
"api_key": "sk-your-openai-key",
|
||||||
"api_base": "https://api.openai.com/v1"
|
"api_base": "https://api.openai.com/v1"
|
||||||
},
|
},
|
||||||
|
|
@ -82,7 +82,7 @@ The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
|
||||||
|
|
||||||
| Prefix | Description | Example |
|
| Prefix | Description | Example |
|
||||||
|--------|-------------|---------|
|
|--------|-------------|---------|
|
||||||
| `openai/` | OpenAI API (default) | `openai/gpt-5.2` |
|
| `openai/` | OpenAI API (default) | `openai/gpt-5.4` |
|
||||||
| `anthropic/` | Anthropic API | `anthropic/claude-opus-4` |
|
| `anthropic/` | Anthropic API | `anthropic/claude-opus-4` |
|
||||||
| `antigravity/` | Google via Antigravity OAuth | `antigravity/gemini-2.0-flash` |
|
| `antigravity/` | Google via Antigravity OAuth | `antigravity/gemini-2.0-flash` |
|
||||||
| `gemini/` | Google Gemini API | `gemini/gemini-2.0-flash-exp` |
|
| `gemini/` | Google Gemini API | `gemini/gemini-2.0-flash-exp` |
|
||||||
|
|
@ -109,7 +109,7 @@ The `model` field uses a protocol prefix format: `[protocol/]model-identifier`
|
||||||
| Field | Required | Description |
|
| Field | Required | Description |
|
||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
| `model_name` | Yes | User-facing alias for the model |
|
| `model_name` | Yes | User-facing alias for the model |
|
||||||
| `model` | Yes | Protocol and model identifier (e.g., `openai/gpt-5.2`) |
|
| `model` | Yes | Protocol and model identifier (e.g., `openai/gpt-5.4`) |
|
||||||
| `api_base` | No | API endpoint URL |
|
| `api_base` | No | API endpoint URL |
|
||||||
| `api_key` | No* | API authentication key |
|
| `api_key` | No* | API authentication key |
|
||||||
| `proxy` | No | HTTP proxy URL |
|
| `proxy` | No | HTTP proxy URL |
|
||||||
|
|
@ -130,19 +130,19 @@ Configure multiple endpoints for the same model to distribute load:
|
||||||
"model_list": [
|
"model_list": [
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-key1",
|
"api_key": "sk-key1",
|
||||||
"api_base": "https://api1.example.com/v1"
|
"api_base": "https://api1.example.com/v1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-key2",
|
"api_key": "sk-key2",
|
||||||
"api_base": "https://api2.example.com/v1"
|
"api_base": "https://api2.example.com/v1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"model_name": "gpt4",
|
"model_name": "gpt4",
|
||||||
"model": "openai/gpt-5.2",
|
"model": "openai/gpt-5.4",
|
||||||
"api_key": "sk-key3",
|
"api_key": "sk-key3",
|
||||||
"api_base": "https://api3.example.com/v1"
|
"api_base": "https://api3.example.com/v1"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,12 @@ func (m *Manager) SendPlaceholder(ctx context.Context, channel, chatID string) b
|
||||||
// Implements PlaceholderRecorder.
|
// Implements PlaceholderRecorder.
|
||||||
func (m *Manager) RecordTypingStop(channel, chatID string, stop func()) {
|
func (m *Manager) RecordTypingStop(channel, chatID string, stop func()) {
|
||||||
key := channel + ":" + chatID
|
key := channel + ":" + chatID
|
||||||
m.typingStops.Store(key, typingEntry{stop: stop, createdAt: time.Now()})
|
entry := typingEntry{stop: stop, createdAt: time.Now()}
|
||||||
|
if previous, loaded := m.typingStops.Swap(key, entry); loaded {
|
||||||
|
if oldEntry, ok := previous.(typingEntry); ok && oldEntry.stop != nil {
|
||||||
|
oldEntry.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RecordReactionUndo registers a reaction undo function for later invocation.
|
// RecordReactionUndo registers a reaction undo function for later invocation.
|
||||||
|
|
|
||||||
|
|
@ -616,6 +616,37 @@ func TestRecordTypingStop_ConcurrentSafe(t *testing.T) {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRecordTypingStop_ReplacesExistingStop(t *testing.T) {
|
||||||
|
m := newTestManager()
|
||||||
|
var oldStopCalls int
|
||||||
|
var newStopCalls int
|
||||||
|
|
||||||
|
m.RecordTypingStop("test", "123", func() {
|
||||||
|
oldStopCalls++
|
||||||
|
})
|
||||||
|
|
||||||
|
m.RecordTypingStop("test", "123", func() {
|
||||||
|
newStopCalls++
|
||||||
|
})
|
||||||
|
|
||||||
|
if oldStopCalls != 1 {
|
||||||
|
t.Fatalf("expected previous typing stop to be called once when replaced, got %d", oldStopCalls)
|
||||||
|
}
|
||||||
|
if newStopCalls != 0 {
|
||||||
|
t.Fatalf("expected replacement typing stop to stay active until preSend, got %d calls", newStopCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := bus.OutboundMessage{Channel: "test", ChatID: "123", Content: "hello"}
|
||||||
|
m.preSend(context.Background(), "test", msg, &mockChannel{})
|
||||||
|
|
||||||
|
if newStopCalls != 1 {
|
||||||
|
t.Fatalf("expected replacement typing stop to be called by preSend, got %d", newStopCalls)
|
||||||
|
}
|
||||||
|
if oldStopCalls != 1 {
|
||||||
|
t.Fatalf("expected previous typing stop to not be called again, got %d", oldStopCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSendWithRetry_PreSendEditsPlaceholder(t *testing.T) {
|
func TestSendWithRetry_PreSendEditsPlaceholder(t *testing.T) {
|
||||||
m := newTestManager()
|
m := newTestManager()
|
||||||
var sendCalled bool
|
var sendCalled bool
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,7 @@ func TestLoadConfig_WebToolsProxy(t *testing.T) {
|
||||||
configPath := filepath.Join(tmpDir, "config.json")
|
configPath := filepath.Join(tmpDir, "config.json")
|
||||||
configJSON := `{
|
configJSON := `{
|
||||||
"agents": {"defaults":{"workspace":"./workspace","model":"gpt4","max_tokens":8192,"max_tool_iterations":20}},
|
"agents": {"defaults":{"workspace":"./workspace","model":"gpt4","max_tokens":8192,"max_tool_iterations":20}},
|
||||||
"model_list": [{"model_name":"gpt4","model":"openai/gpt-5.2","api_key":"x"}],
|
"model_list": [{"model_name":"gpt4","model":"openai/gpt-5.4","api_key":"x"}],
|
||||||
"tools": {"web":{"proxy":"http://127.0.0.1:7890"}}
|
"tools": {"web":{"proxy":"http://127.0.0.1:7890"}}
|
||||||
}`
|
}`
|
||||||
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
if err := os.WriteFile(configPath, []byte(configJSON), 0o600); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -194,8 +194,8 @@ func DefaultConfig() *Config {
|
||||||
|
|
||||||
// OpenAI - https://platform.openai.com/api-keys
|
// OpenAI - https://platform.openai.com/api-keys
|
||||||
{
|
{
|
||||||
ModelName: "gpt-5.2",
|
ModelName: "gpt-5.4",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
APIBase: "https://api.openai.com/v1",
|
APIBase: "https://api.openai.com/v1",
|
||||||
APIKey: "",
|
APIKey: "",
|
||||||
},
|
},
|
||||||
|
|
@ -256,8 +256,8 @@ func DefaultConfig() *Config {
|
||||||
APIKey: "",
|
APIKey: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ModelName: "openrouter-gpt-5.2",
|
ModelName: "openrouter-gpt-5.4",
|
||||||
Model: "openrouter/openai/gpt-5.2",
|
Model: "openrouter/openai/gpt-5.4",
|
||||||
APIBase: "https://openrouter.ai/api/v1",
|
APIBase: "https://openrouter.ai/api/v1",
|
||||||
APIKey: "",
|
APIKey: "",
|
||||||
},
|
},
|
||||||
|
|
@ -287,6 +287,12 @@ func DefaultConfig() *Config {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Volcengine (火山引擎) - https://console.volcengine.com/ark
|
// Volcengine (火山引擎) - https://console.volcengine.com/ark
|
||||||
|
{
|
||||||
|
ModelName: "ark-code-latest",
|
||||||
|
Model: "volcengine/ark-code-latest",
|
||||||
|
APIBase: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
APIKey: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ModelName: "doubao-pro",
|
ModelName: "doubao-pro",
|
||||||
Model: "volcengine/doubao-pro-32k",
|
Model: "volcengine/doubao-pro-32k",
|
||||||
|
|
@ -311,8 +317,8 @@ func DefaultConfig() *Config {
|
||||||
|
|
||||||
// GitHub Copilot - https://github.com/settings/tokens
|
// GitHub Copilot - https://github.com/settings/tokens
|
||||||
{
|
{
|
||||||
ModelName: "copilot-gpt-5.2",
|
ModelName: "copilot-gpt-5.4",
|
||||||
Model: "github-copilot/gpt-5.2",
|
Model: "github-copilot/gpt-5.4",
|
||||||
APIBase: "http://localhost:4321",
|
APIBase: "http://localhost:4321",
|
||||||
AuthMethod: "oauth",
|
AuthMethod: "oauth",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
|
||||||
}
|
}
|
||||||
return ModelConfig{
|
return ModelConfig{
|
||||||
ModelName: "openai",
|
ModelName: "openai",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
APIKey: p.OpenAI.APIKey,
|
APIKey: p.OpenAI.APIKey,
|
||||||
APIBase: p.OpenAI.APIBase,
|
APIBase: p.OpenAI.APIBase,
|
||||||
Proxy: p.OpenAI.Proxy,
|
Proxy: p.OpenAI.Proxy,
|
||||||
|
|
@ -335,7 +335,7 @@ func ConvertProvidersToModelList(cfg *Config) []ModelConfig {
|
||||||
}
|
}
|
||||||
return ModelConfig{
|
return ModelConfig{
|
||||||
ModelName: "github-copilot",
|
ModelName: "github-copilot",
|
||||||
Model: "github-copilot/gpt-5.2",
|
Model: "github-copilot/gpt-5.4",
|
||||||
APIBase: p.GitHubCopilot.APIBase,
|
APIBase: p.GitHubCopilot.APIBase,
|
||||||
ConnectMode: p.GitHubCopilot.ConnectMode,
|
ConnectMode: p.GitHubCopilot.ConnectMode,
|
||||||
}, true
|
}, true
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ func TestConvertProvidersToModelList_OpenAI(t *testing.T) {
|
||||||
if result[0].ModelName != "openai" {
|
if result[0].ModelName != "openai" {
|
||||||
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "openai")
|
t.Errorf("ModelName = %q, want %q", result[0].ModelName, "openai")
|
||||||
}
|
}
|
||||||
if result[0].Model != "openai/gpt-5.2" {
|
if result[0].Model != "openai/gpt-5.4" {
|
||||||
t.Errorf("Model = %q, want %q", result[0].Model, "openai/gpt-5.2")
|
t.Errorf("Model = %q, want %q", result[0].Model, "openai/gpt-5.4")
|
||||||
}
|
}
|
||||||
if result[0].APIKey != "sk-test-key" {
|
if result[0].APIKey != "sk-test-key" {
|
||||||
t.Errorf("APIKey = %q, want %q", result[0].APIKey, "sk-test-key")
|
t.Errorf("APIKey = %q, want %q", result[0].APIKey, "sk-test-key")
|
||||||
|
|
@ -384,8 +384,8 @@ func TestConvertProvidersToModelList_MultipleProviders_PreservesUserModel(t *tes
|
||||||
for _, mc := range result {
|
for _, mc := range result {
|
||||||
switch mc.ModelName {
|
switch mc.ModelName {
|
||||||
case "openai":
|
case "openai":
|
||||||
if mc.Model != "openai/gpt-5.2" {
|
if mc.Model != "openai/gpt-5.4" {
|
||||||
t.Errorf("OpenAI Model = %q, want %q (default)", mc.Model, "openai/gpt-5.2")
|
t.Errorf("OpenAI Model = %q, want %q (default)", mc.Model, "openai/gpt-5.4")
|
||||||
}
|
}
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
if mc.Model != "deepseek/deepseek-reasoner" {
|
if mc.Model != "deepseek/deepseek-reasoner" {
|
||||||
|
|
@ -558,9 +558,9 @@ func TestConvertProvidersToModelList_NoProviderField_NoModel(t *testing.T) {
|
||||||
// Tests for buildModelWithProtocol helper function
|
// Tests for buildModelWithProtocol helper function
|
||||||
|
|
||||||
func TestBuildModelWithProtocol_NoPrefix(t *testing.T) {
|
func TestBuildModelWithProtocol_NoPrefix(t *testing.T) {
|
||||||
result := buildModelWithProtocol("openai", "gpt-5.2")
|
result := buildModelWithProtocol("openai", "gpt-5.4")
|
||||||
if result != "openai/gpt-5.2" {
|
if result != "openai/gpt-5.4" {
|
||||||
t.Errorf("buildModelWithProtocol(openai, gpt-5.2) = %q, want %q", result, "openai/gpt-5.2")
|
t.Errorf("buildModelWithProtocol(openai, gpt-5.4) = %q, want %q", result, "openai/gpt-5.4")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -490,7 +490,7 @@ echo '{"type":"turn.completed"}'`
|
||||||
}
|
}
|
||||||
|
|
||||||
messages := []Message{{Role: "user", Content: "test"}}
|
messages := []Message{{Role: "user", Content: "test"}}
|
||||||
_, err := p.Chat(context.Background(), messages, nil, "gpt-5.2-codex", nil)
|
_, err := p.Chat(context.Background(), messages, nil, "gpt-5.3-codex", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -502,7 +502,7 @@ echo '{"type":"turn.completed"}'`
|
||||||
}
|
}
|
||||||
args := string(argsData)
|
args := string(argsData)
|
||||||
|
|
||||||
if !strings.Contains(args, "-m gpt-5.2-codex") {
|
if !strings.Contains(args, "-m gpt-5.3-codex") {
|
||||||
t.Errorf("args should contain model flag, got: %s", args)
|
t.Errorf("args should contain model flag, got: %s", args)
|
||||||
}
|
}
|
||||||
if !strings.Contains(args, "-C /tmp/test-workspace") {
|
if !strings.Contains(args, "-C /tmp/test-workspace") {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
codexDefaultModel = "gpt-5.2"
|
codexDefaultModel = "gpt-5.3-codex"
|
||||||
codexDefaultInstructions = "You are Codex, a coding assistant."
|
codexDefaultInstructions = "You are Codex, a coding assistant."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -568,7 +568,7 @@ func TestCodexProvider_ChatRoundTrip_ModelFallbackFromUnsupported(t *testing.T)
|
||||||
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
provider.client = createOpenAITestClient(server.URL, "test-token", "acc-123")
|
||||||
|
|
||||||
messages := []Message{{Role: "user", Content: "Hello"}}
|
messages := []Message{{Role: "user", Content: "Hello"}}
|
||||||
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-5.2", nil)
|
resp, err := provider.Chat(t.Context(), messages, nil, "gpt-5.3-codex", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Chat() error: %v", err)
|
t.Fatalf("Chat() error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -599,7 +599,7 @@ func TestResolveCodexModel(t *testing.T) {
|
||||||
wantFallback: true,
|
wantFallback: true,
|
||||||
},
|
},
|
||||||
{name: "non-openai prefixed", input: "glm-4.7", wantModel: codexDefaultModel, wantFallback: true},
|
{name: "non-openai prefixed", input: "glm-4.7", wantModel: codexDefaultModel, wantFallback: true},
|
||||||
{name: "openai prefix", input: "openai/gpt-5.2", wantModel: "gpt-5.2", wantFallback: false},
|
{name: "openai prefix", input: "openai/gpt-5.3-codex", wantModel: "gpt-5.3-codex", wantFallback: false},
|
||||||
{name: "direct gpt", input: "gpt-4o", wantModel: "gpt-4o", wantFallback: false},
|
{name: "direct gpt", input: "gpt-4o", wantModel: "gpt-4o", wantFallback: false},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,7 @@ func TestGatewayStartReady_OAuthModelRequiresStoredCredential(t *testing.T) {
|
||||||
}
|
}
|
||||||
cfg.ModelList = []config.ModelConfig{{
|
cfg.ModelList = []config.ModelConfig{{
|
||||||
ModelName: "openai-oauth",
|
ModelName: "openai-oauth",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
AuthMethod: "oauth",
|
AuthMethod: "oauth",
|
||||||
}}
|
}}
|
||||||
cfg.Agents.Defaults.ModelName = "openai-oauth"
|
cfg.Agents.Defaults.ModelName = "openai-oauth"
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ func TestHandleListModels_ConfiguredStatusUsesRuntimeProbesForLocalModels(t *tes
|
||||||
cfg.ModelList = []config.ModelConfig{
|
cfg.ModelList = []config.ModelConfig{
|
||||||
{
|
{
|
||||||
ModelName: "openai-oauth",
|
ModelName: "openai-oauth",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
AuthMethod: "oauth",
|
AuthMethod: "oauth",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -81,8 +81,8 @@ func TestHandleListModels_ConfiguredStatusUsesRuntimeProbesForLocalModels(t *tes
|
||||||
APIKey: "remote-key",
|
APIKey: "remote-key",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ModelName: "copilot-gpt-5.2",
|
ModelName: "copilot-gpt-5.4",
|
||||||
Model: "github-copilot/gpt-5.2",
|
Model: "github-copilot/gpt-5.4",
|
||||||
APIBase: "http://127.0.0.1:4321",
|
APIBase: "http://127.0.0.1:4321",
|
||||||
AuthMethod: "oauth",
|
AuthMethod: "oauth",
|
||||||
},
|
},
|
||||||
|
|
@ -128,7 +128,7 @@ func TestHandleListModels_ConfiguredStatusUsesRuntimeProbesForLocalModels(t *tes
|
||||||
if !got["vllm-remote"] {
|
if !got["vllm-remote"] {
|
||||||
t.Fatalf("remote vllm model configured = false, want true with api_key")
|
t.Fatalf("remote vllm model configured = false, want true with api_key")
|
||||||
}
|
}
|
||||||
if !got["copilot-gpt-5.2"] {
|
if !got["copilot-gpt-5.4"] {
|
||||||
t.Fatalf("copilot model configured = false, want true when local bridge probe succeeds")
|
t.Fatalf("copilot model configured = false, want true when local bridge probe succeeds")
|
||||||
}
|
}
|
||||||
if len(openAIProbes) != 1 || openAIProbes[0] != "http://127.0.0.1:8000/v1|custom-model" {
|
if len(openAIProbes) != 1 || openAIProbes[0] != "http://127.0.0.1:8000/v1|custom-model" {
|
||||||
|
|
|
||||||
|
|
@ -791,8 +791,8 @@ func defaultModelConfigForProvider(provider, authMethod string) config.ModelConf
|
||||||
switch provider {
|
switch provider {
|
||||||
case oauthProviderOpenAI:
|
case oauthProviderOpenAI:
|
||||||
return config.ModelConfig{
|
return config.ModelConfig{
|
||||||
ModelName: "gpt-5.2",
|
ModelName: "gpt-5.4",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
AuthMethod: authMethod,
|
AuthMethod: authMethod,
|
||||||
}
|
}
|
||||||
case oauthProviderAnthropic:
|
case oauthProviderAnthropic:
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,8 @@ func TestOAuthLogoutClearsCredentialAndConfig(t *testing.T) {
|
||||||
}
|
}
|
||||||
cfg.Providers.OpenAI.AuthMethod = "oauth"
|
cfg.Providers.OpenAI.AuthMethod = "oauth"
|
||||||
cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
|
cfg.ModelList = append(cfg.ModelList, config.ModelConfig{
|
||||||
ModelName: "gpt-5.2",
|
ModelName: "gpt-5.4",
|
||||||
Model: "openai/gpt-5.2",
|
Model: "openai/gpt-5.4",
|
||||||
AuthMethod: "oauth",
|
AuthMethod: "oauth",
|
||||||
})
|
})
|
||||||
if err = config.SaveConfig(configPath, cfg); err != nil {
|
if err = config.SaveConfig(configPath, cfg); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
setLauncherConfig as updateLauncherConfig,
|
setLauncherConfig as updateLauncherConfig,
|
||||||
} from "@/api/system"
|
} from "@/api/system"
|
||||||
import {
|
import {
|
||||||
AdvancedSection,
|
|
||||||
AgentDefaultsSection,
|
AgentDefaultsSection,
|
||||||
DevicesSection,
|
DevicesSection,
|
||||||
LauncherSection,
|
LauncherSection,
|
||||||
|
|
@ -30,7 +29,6 @@ import {
|
||||||
} from "@/components/config/form-model"
|
} from "@/components/config/form-model"
|
||||||
import { PageHeader } from "@/components/page-header"
|
import { PageHeader } from "@/components/page-header"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Separator } from "@/components/ui/separator"
|
|
||||||
|
|
||||||
export function ConfigPage() {
|
export function ConfigPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
@ -56,11 +54,7 @@ export function ConfigPage() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const { data: launcherConfig, isLoading: isLauncherLoading } = useQuery({
|
||||||
data: launcherConfig,
|
|
||||||
isLoading: isLauncherLoading,
|
|
||||||
error: launcherError,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["system", "launcher-config"],
|
queryKey: ["system", "launcher-config"],
|
||||||
queryFn: getLauncherConfig,
|
queryFn: getLauncherConfig,
|
||||||
})
|
})
|
||||||
|
|
@ -111,10 +105,6 @@ export function ConfigPage() {
|
||||||
? t("pages.config.autostart_unsupported")
|
? t("pages.config.autostart_unsupported")
|
||||||
: t("pages.config.autostart_hint")
|
: t("pages.config.autostart_hint")
|
||||||
|
|
||||||
const launcherHint = launcherError
|
|
||||||
? t("pages.config.launcher_load_error")
|
|
||||||
: t("pages.config.launcher_restart_hint")
|
|
||||||
|
|
||||||
const updateField = <K extends keyof CoreConfigForm>(
|
const updateField = <K extends keyof CoreConfigForm>(
|
||||||
key: K,
|
key: K,
|
||||||
value: CoreConfigForm[K],
|
value: CoreConfigForm[K],
|
||||||
|
|
@ -287,21 +277,14 @@ export function ConfigPage() {
|
||||||
|
|
||||||
<AgentDefaultsSection form={form} onFieldChange={updateField} />
|
<AgentDefaultsSection form={form} onFieldChange={updateField} />
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<RuntimeSection form={form} onFieldChange={updateField} />
|
<RuntimeSection form={form} onFieldChange={updateField} />
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<LauncherSection
|
<LauncherSection
|
||||||
launcherForm={launcherForm}
|
launcherForm={launcherForm}
|
||||||
onFieldChange={updateLauncherField}
|
onFieldChange={updateLauncherField}
|
||||||
launcherHint={launcherHint}
|
|
||||||
disabled={saving || isLauncherLoading}
|
disabled={saving || isLauncherLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<DevicesSection
|
<DevicesSection
|
||||||
form={form}
|
form={form}
|
||||||
onFieldChange={updateField}
|
onFieldChange={updateField}
|
||||||
|
|
@ -316,10 +299,6 @@ export function ConfigPage() {
|
||||||
onAutoStartChange={setAutoStartEnabled}
|
onAutoStartChange={setAutoStartEnabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<AdvancedSection />
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { IconCode } from "@tabler/icons-react"
|
import type { ReactNode } from "react"
|
||||||
import { Link } from "@tanstack/react-router"
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,7 +7,13 @@ import {
|
||||||
type LauncherForm,
|
type LauncherForm,
|
||||||
} from "@/components/config/form-model"
|
} from "@/components/config/form-model"
|
||||||
import { Field, SwitchCardField } from "@/components/shared-form"
|
import { Field, SwitchCardField } from "@/components/shared-form"
|
||||||
import { Button } from "@/components/ui/button"
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
|
|
@ -29,6 +34,30 @@ type UpdateLauncherField = <K extends keyof LauncherForm>(
|
||||||
value: LauncherForm[K],
|
value: LauncherForm[K],
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
|
interface ConfigSectionCardProps {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConfigSectionCard({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
}: ConfigSectionCardProps) {
|
||||||
|
return (
|
||||||
|
<Card size="sm">
|
||||||
|
<CardHeader className="border-border border-b">
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
{description && <CardDescription>{description}</CardDescription>}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="divide-border/70 divide-y">{children}</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
interface AgentDefaultsSectionProps {
|
interface AgentDefaultsSectionProps {
|
||||||
form: CoreConfigForm
|
form: CoreConfigForm
|
||||||
onFieldChange: UpdateCoreField
|
onFieldChange: UpdateCoreField
|
||||||
|
|
@ -41,89 +70,94 @@ export function AgentDefaultsSection({
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3">
|
<ConfigSectionCard title={t("pages.config.sections.agent")}>
|
||||||
<div className="space-y-4">
|
<Field
|
||||||
<Field
|
label={t("pages.config.workspace")}
|
||||||
label={t("pages.config.workspace")}
|
hint={t("pages.config.workspace_hint")}
|
||||||
hint={t("pages.config.workspace_hint")}
|
layout="setting-row"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
value={form.workspace}
|
value={form.workspace}
|
||||||
onChange={(e) => onFieldChange("workspace", e.target.value)}
|
onChange={(e) => onFieldChange("workspace", e.target.value)}
|
||||||
placeholder="~/.picoclaw/workspace"
|
placeholder="~/.picoclaw/workspace"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<SwitchCardField
|
<SwitchCardField
|
||||||
label={t("pages.config.restrict_workspace")}
|
label={t("pages.config.restrict_workspace")}
|
||||||
hint={t("pages.config.restrict_workspace_hint")}
|
hint={t("pages.config.restrict_workspace_hint")}
|
||||||
checked={form.restrictToWorkspace}
|
layout="setting-row"
|
||||||
onCheckedChange={(checked) =>
|
checked={form.restrictToWorkspace}
|
||||||
onFieldChange("restrictToWorkspace", checked)
|
onCheckedChange={(checked) =>
|
||||||
|
onFieldChange("restrictToWorkspace", checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SwitchCardField
|
||||||
|
label={t("pages.config.allow_remote")}
|
||||||
|
hint={t("pages.config.allow_remote_hint")}
|
||||||
|
layout="setting-row"
|
||||||
|
checked={form.allowRemote}
|
||||||
|
onCheckedChange={(checked) => onFieldChange("allowRemote", checked)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={t("pages.config.max_tokens")}
|
||||||
|
hint={t("pages.config.max_tokens_hint")}
|
||||||
|
layout="setting-row"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={form.maxTokens}
|
||||||
|
onChange={(e) => onFieldChange("maxTokens", e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={t("pages.config.max_tool_iterations")}
|
||||||
|
hint={t("pages.config.max_tool_iterations_hint")}
|
||||||
|
layout="setting-row"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={form.maxToolIterations}
|
||||||
|
onChange={(e) => onFieldChange("maxToolIterations", e.target.value)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label={t("pages.config.summarize_threshold")}
|
||||||
|
hint={t("pages.config.summarize_threshold_hint")}
|
||||||
|
layout="setting-row"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={form.summarizeMessageThreshold}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFieldChange("summarizeMessageThreshold", e.target.value)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<SwitchCardField
|
<Field
|
||||||
label={t("pages.config.allow_remote")}
|
label={t("pages.config.summarize_token_percent")}
|
||||||
hint={t("pages.config.allow_remote_hint")}
|
hint={t("pages.config.summarize_token_percent_hint")}
|
||||||
checked={form.allowRemote}
|
layout="setting-row"
|
||||||
onCheckedChange={(checked) => onFieldChange("allowRemote", checked)}
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
value={form.summarizeTokenPercent}
|
||||||
|
onChange={(e) =>
|
||||||
|
onFieldChange("summarizeTokenPercent", e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
</Field>
|
||||||
<Field
|
</ConfigSectionCard>
|
||||||
label={t("pages.config.max_tokens")}
|
|
||||||
hint={t("pages.config.max_tokens_hint")}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={form.maxTokens}
|
|
||||||
onChange={(e) => onFieldChange("maxTokens", e.target.value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label={t("pages.config.max_tool_iterations")}
|
|
||||||
hint={t("pages.config.max_tool_iterations_hint")}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={form.maxToolIterations}
|
|
||||||
onChange={(e) => onFieldChange("maxToolIterations", e.target.value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label={t("pages.config.summarize_threshold")}
|
|
||||||
hint={t("pages.config.summarize_threshold_hint")}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={form.summarizeMessageThreshold}
|
|
||||||
onChange={(e) =>
|
|
||||||
onFieldChange("summarizeMessageThreshold", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
label={t("pages.config.summarize_token_percent")}
|
|
||||||
hint={t("pages.config.summarize_token_percent_hint")}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
value={form.summarizeTokenPercent}
|
|
||||||
onChange={(e) =>
|
|
||||||
onFieldChange("summarizeTokenPercent", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -139,126 +173,123 @@ export function RuntimeSection({ form, onFieldChange }: RuntimeSectionProps) {
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3">
|
<ConfigSectionCard title={t("pages.config.sections.runtime")}>
|
||||||
<div className="space-y-4">
|
<Field
|
||||||
<Field
|
label={t("pages.config.session_scope")}
|
||||||
label={t("pages.config.session_scope")}
|
hint={t("pages.config.session_scope_hint")}
|
||||||
hint={t("pages.config.session_scope_hint")}
|
layout="setting-row"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={form.dmScope}
|
||||||
|
onValueChange={(value) => onFieldChange("dmScope", value)}
|
||||||
>
|
>
|
||||||
<Select
|
<SelectTrigger className="w-full">
|
||||||
value={form.dmScope}
|
<SelectValue>
|
||||||
onValueChange={(value) => onFieldChange("dmScope", value)}
|
{selectedDmScopeOption
|
||||||
>
|
? t(
|
||||||
<SelectTrigger>
|
selectedDmScopeOption.labelKey,
|
||||||
<SelectValue>
|
selectedDmScopeOption.labelDefault,
|
||||||
{selectedDmScopeOption
|
)
|
||||||
? t(
|
: form.dmScope}
|
||||||
selectedDmScopeOption.labelKey,
|
</SelectValue>
|
||||||
selectedDmScopeOption.labelDefault,
|
</SelectTrigger>
|
||||||
)
|
<SelectContent>
|
||||||
: form.dmScope}
|
{DM_SCOPE_OPTIONS.map((scope) => (
|
||||||
</SelectValue>
|
<SelectItem key={scope.value} value={scope.value}>
|
||||||
</SelectTrigger>
|
<div className="flex flex-col gap-0.5">
|
||||||
<SelectContent>
|
<span className="font-medium">{t(scope.labelKey)}</span>
|
||||||
{DM_SCOPE_OPTIONS.map((scope) => (
|
<span className="text-muted-foreground text-xs">
|
||||||
<SelectItem key={scope.value} value={scope.value}>
|
{t(scope.descKey)}
|
||||||
<div className="flex flex-col gap-0.5">
|
</span>
|
||||||
<span className="font-medium">{t(scope.labelKey)}</span>
|
</div>
|
||||||
<span className="text-muted-foreground text-xs">
|
</SelectItem>
|
||||||
{t(scope.descKey)}
|
))}
|
||||||
</span>
|
</SelectContent>
|
||||||
</div>
|
</Select>
|
||||||
</SelectItem>
|
</Field>
|
||||||
))}
|
|
||||||
</SelectContent>
|
<SwitchCardField
|
||||||
</Select>
|
label={t("pages.config.heartbeat_enabled")}
|
||||||
|
hint={t("pages.config.heartbeat_enabled_hint")}
|
||||||
|
layout="setting-row"
|
||||||
|
checked={form.heartbeatEnabled}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onFieldChange("heartbeatEnabled", checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.heartbeatEnabled && (
|
||||||
|
<Field
|
||||||
|
label={t("pages.config.heartbeat_interval")}
|
||||||
|
hint={t("pages.config.heartbeat_interval_hint")}
|
||||||
|
layout="setting-row"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={form.heartbeatInterval}
|
||||||
|
onChange={(e) => onFieldChange("heartbeatInterval", e.target.value)}
|
||||||
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
)}
|
||||||
<SwitchCardField
|
</ConfigSectionCard>
|
||||||
label={t("pages.config.heartbeat_enabled")}
|
|
||||||
hint={t("pages.config.heartbeat_enabled_hint")}
|
|
||||||
checked={form.heartbeatEnabled}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onFieldChange("heartbeatEnabled", checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{form.heartbeatEnabled && (
|
|
||||||
<Field
|
|
||||||
label={t("pages.config.heartbeat_interval")}
|
|
||||||
hint={t("pages.config.heartbeat_interval_hint")}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
value={form.heartbeatInterval}
|
|
||||||
onChange={(e) =>
|
|
||||||
onFieldChange("heartbeatInterval", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LauncherSectionProps {
|
interface LauncherSectionProps {
|
||||||
launcherForm: LauncherForm
|
launcherForm: LauncherForm
|
||||||
onFieldChange: UpdateLauncherField
|
onFieldChange: UpdateLauncherField
|
||||||
launcherHint: string
|
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LauncherSection({
|
export function LauncherSection({
|
||||||
launcherForm,
|
launcherForm,
|
||||||
onFieldChange,
|
onFieldChange,
|
||||||
launcherHint,
|
|
||||||
disabled,
|
disabled,
|
||||||
}: LauncherSectionProps) {
|
}: LauncherSectionProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3">
|
<ConfigSectionCard title={t("pages.config.sections.launcher")}>
|
||||||
<div className="space-y-4">
|
<SwitchCardField
|
||||||
<Field
|
label={t("pages.config.lan_access")}
|
||||||
label={t("pages.config.server_port")}
|
hint={t("pages.config.lan_access_hint")}
|
||||||
hint={t("pages.config.server_port_hint")}
|
layout="setting-row"
|
||||||
>
|
checked={launcherForm.publicAccess}
|
||||||
<Input
|
disabled={disabled}
|
||||||
type="number"
|
onCheckedChange={(checked) => onFieldChange("publicAccess", checked)}
|
||||||
min={1}
|
/>
|
||||||
max={65535}
|
|
||||||
value={launcherForm.port}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={(e) => onFieldChange("port", e.target.value)}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<SwitchCardField
|
<Field
|
||||||
label={t("pages.config.lan_access")}
|
label={t("pages.config.server_port")}
|
||||||
hint={t("pages.config.lan_access_hint")}
|
hint={t("pages.config.server_port_hint")}
|
||||||
checked={launcherForm.publicAccess}
|
layout="setting-row"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={65535}
|
||||||
|
value={launcherForm.port}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onCheckedChange={(checked) => onFieldChange("publicAccess", checked)}
|
onChange={(e) => onFieldChange("port", e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label={t("pages.config.allowed_cidrs")}
|
label={t("pages.config.allowed_cidrs")}
|
||||||
hint={t("pages.config.allowed_cidrs_hint")}
|
hint={t("pages.config.allowed_cidrs_hint")}
|
||||||
>
|
layout="setting-row"
|
||||||
<Textarea
|
controlClassName="md:max-w-md"
|
||||||
value={launcherForm.allowedCIDRsText}
|
>
|
||||||
disabled={disabled}
|
<Textarea
|
||||||
placeholder={t("pages.config.allowed_cidrs_placeholder")}
|
value={launcherForm.allowedCIDRsText}
|
||||||
className="min-h-[88px]"
|
disabled={disabled}
|
||||||
onChange={(e) => onFieldChange("allowedCIDRsText", e.target.value)}
|
placeholder={t("pages.config.allowed_cidrs_placeholder")}
|
||||||
/>
|
className="min-h-[88px]"
|
||||||
</Field>
|
onChange={(e) => onFieldChange("allowedCIDRsText", e.target.value)}
|
||||||
|
/>
|
||||||
<p className="text-muted-foreground text-xs">{launcherHint}</p>
|
</Field>
|
||||||
</div>
|
</ConfigSectionCard>
|
||||||
</section>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,52 +313,31 @@ export function DevicesSection({
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-3">
|
<ConfigSectionCard title={t("pages.config.sections.devices")}>
|
||||||
<div className="space-y-4">
|
<SwitchCardField
|
||||||
<SwitchCardField
|
label={t("pages.config.devices_enabled")}
|
||||||
label={t("pages.config.devices_enabled")}
|
hint={t("pages.config.devices_enabled_hint")}
|
||||||
hint={t("pages.config.devices_enabled_hint")}
|
layout="setting-row"
|
||||||
checked={form.devicesEnabled}
|
checked={form.devicesEnabled}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) => onFieldChange("devicesEnabled", checked)}
|
||||||
onFieldChange("devicesEnabled", checked)
|
/>
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SwitchCardField
|
<SwitchCardField
|
||||||
label={t("pages.config.monitor_usb")}
|
label={t("pages.config.monitor_usb")}
|
||||||
hint={t("pages.config.monitor_usb_hint")}
|
hint={t("pages.config.monitor_usb_hint")}
|
||||||
checked={form.monitorUSB}
|
layout="setting-row"
|
||||||
onCheckedChange={(checked) => onFieldChange("monitorUSB", checked)}
|
checked={form.monitorUSB}
|
||||||
/>
|
onCheckedChange={(checked) => onFieldChange("monitorUSB", checked)}
|
||||||
|
/>
|
||||||
|
|
||||||
<SwitchCardField
|
<SwitchCardField
|
||||||
label={t("pages.config.autostart_label")}
|
label={t("pages.config.autostart_label")}
|
||||||
hint={autoStartHint}
|
hint={autoStartHint}
|
||||||
checked={autoStartEnabled}
|
layout="setting-row"
|
||||||
disabled={autoStartDisabled}
|
checked={autoStartEnabled}
|
||||||
onCheckedChange={onAutoStartChange}
|
disabled={autoStartDisabled}
|
||||||
/>
|
onCheckedChange={onAutoStartChange}
|
||||||
</div>
|
/>
|
||||||
</section>
|
</ConfigSectionCard>
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AdvancedSection() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="space-y-3">
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
{t("pages.config.advanced_desc")}
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<Button variant="outline" asChild>
|
|
||||||
<Link to="/config/raw">
|
|
||||||
<IconCode className="size-4" />
|
|
||||||
{t("pages.config.open_raw")}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
import { IconAdjustments } from "@tabler/icons-react"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { Link } from "@tanstack/react-router"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
|
import { PageHeader } from "@/components/page-header"
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
@ -15,17 +18,9 @@ import {
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card"
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
export function RawJsonPanel() {
|
export function RawConfigPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
|
@ -124,81 +119,89 @@ export function RawJsonPanel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="flex h-full flex-col">
|
||||||
<CardHeader>
|
<PageHeader title={t("pages.config.raw_json_title")}>
|
||||||
<CardTitle>{t("pages.config.raw_json_title")}</CardTitle>
|
<Button variant="outline" asChild>
|
||||||
<CardDescription>{t("pages.config.raw_json_desc")}</CardDescription>
|
<Link to="/config">
|
||||||
</CardHeader>
|
<IconAdjustments className="size-4" />
|
||||||
<CardContent>
|
{t("pages.config.back_to_visual")}
|
||||||
{isLoading ? (
|
</Link>
|
||||||
<div className="flex h-64 items-center justify-center">
|
</Button>
|
||||||
<p>{t("labels.loading")}</p>
|
</PageHeader>
|
||||||
</div>
|
|
||||||
) : (
|
<div className="flex min-h-0 flex-1 flex-col p-1 lg:p-3 lg:p-6">
|
||||||
<div className="space-y-3">
|
<div className="mx-auto flex h-full min-h-0 w-full max-w-[1000px] flex-col">
|
||||||
{isDirty && (
|
{isLoading ? (
|
||||||
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-2 text-sm text-yellow-700">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
{t("pages.config.unsaved_changes")}
|
<p>{t("labels.loading")}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
<div className="bg-muted/30 relative rounded-lg border">
|
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||||
<ScrollArea className="h-[calc(100vh-20rem)] min-h-[200px]">
|
{isDirty && (
|
||||||
|
<div className="shrink-0 rounded-lg border border-yellow-200 bg-yellow-50 p-2 text-sm text-yellow-700">
|
||||||
|
{t("pages.config.unsaved_changes")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="relative min-h-0 flex-1 overflow-hidden rounded-lg border shadow-sm">
|
||||||
<Textarea
|
<Textarea
|
||||||
value={effectiveEditorValue}
|
value={effectiveEditorValue}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setEditorValue(e.target.value)
|
setEditorValue(e.target.value)
|
||||||
setIsDirty(true)
|
setIsDirty(true)
|
||||||
}}
|
}}
|
||||||
className="min-h-[200px] resize-none border-0 bg-transparent px-4 py-3 font-mono text-sm shadow-none focus-visible:ring-0"
|
wrap="off"
|
||||||
|
className="h-full min-h-0 resize-none overflow-auto border-0 bg-transparent px-4 py-3 font-mono text-sm [overflow-wrap:normal] whitespace-pre shadow-none focus-visible:ring-0"
|
||||||
placeholder={t("pages.config.json_placeholder")}
|
placeholder={t("pages.config.json_placeholder")}
|
||||||
/>
|
/>
|
||||||
</ScrollArea>
|
</div>
|
||||||
|
<div className="flex shrink-0 justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleFormat}
|
||||||
|
disabled={mutation.isPending}
|
||||||
|
>
|
||||||
|
{t("pages.config.format")}
|
||||||
|
</Button>
|
||||||
|
<AlertDialog
|
||||||
|
open={showResetDialog}
|
||||||
|
onOpenChange={setShowResetDialog}
|
||||||
|
>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={!isDirty}
|
||||||
|
onClick={() => setShowResetDialog(true)}
|
||||||
|
>
|
||||||
|
{t("common.reset")}
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
{t("pages.config.reset_confirm_title")}
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("pages.config.reset_confirm_desc")}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>
|
||||||
|
{t("common.cancel")}
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={confirmReset}>
|
||||||
|
{t("common.confirm")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
<Button onClick={handleSave} disabled={mutation.isPending}>
|
||||||
|
{mutation.isPending ? t("common.saving") : t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end space-x-2">
|
)}
|
||||||
<Button
|
</div>
|
||||||
variant="outline"
|
</div>
|
||||||
onClick={handleFormat}
|
</div>
|
||||||
disabled={mutation.isPending}
|
|
||||||
>
|
|
||||||
{t("pages.config.format")}
|
|
||||||
</Button>
|
|
||||||
<AlertDialog
|
|
||||||
open={showResetDialog}
|
|
||||||
onOpenChange={setShowResetDialog}
|
|
||||||
>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
disabled={!isDirty}
|
|
||||||
onClick={() => setShowResetDialog(true)}
|
|
||||||
>
|
|
||||||
{t("common.reset")}
|
|
||||||
</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>
|
|
||||||
{t("pages.config.reset_confirm_title")}
|
|
||||||
</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
{t("pages.config.reset_confirm_desc")}
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
||||||
<AlertDialogAction onClick={confirmReset}>
|
|
||||||
{t("common.confirm")}
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
<Button onClick={handleSave} disabled={mutation.isPending}>
|
|
||||||
{mutation.isPending ? t("common.saving") : t("common.save")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,9 @@ import {
|
||||||
} from "@/components/ui/field"
|
} from "@/components/ui/field"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Switch } from "@/components/ui/switch"
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
type FieldLayout = "default" | "setting-row"
|
||||||
|
|
||||||
interface FieldProps {
|
interface FieldProps {
|
||||||
label: string
|
label: string
|
||||||
|
|
@ -16,9 +19,45 @@ interface FieldProps {
|
||||||
error?: string
|
error?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
layout?: FieldLayout
|
||||||
|
controlClassName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Field({ label, hint, error, required, children }: FieldProps) {
|
export function Field({
|
||||||
|
label,
|
||||||
|
hint,
|
||||||
|
error,
|
||||||
|
required,
|
||||||
|
children,
|
||||||
|
layout = "default",
|
||||||
|
controlClassName,
|
||||||
|
}: FieldProps) {
|
||||||
|
if (layout === "setting-row") {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 py-4 md:grid md:grid-cols-[minmax(0,1fr)_minmax(240px,320px)] md:items-center md:gap-6">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<FieldLabel>
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-destructive ml-1">*</span>}
|
||||||
|
</FieldLabel>
|
||||||
|
{hint && (
|
||||||
|
<FieldDescription className="text-xs leading-normal">
|
||||||
|
{hint}
|
||||||
|
</FieldDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={cn("w-full md:justify-self-center", controlClassName)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<FieldDescription className="text-destructive text-xs leading-normal md:col-start-2">
|
||||||
|
{error}
|
||||||
|
</FieldDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UiField className="gap-2.5">
|
<UiField className="gap-2.5">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -85,6 +124,7 @@ interface SwitchCardFieldProps {
|
||||||
ariaLabel?: string
|
ariaLabel?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
|
layout?: FieldLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SwitchCardField({
|
export function SwitchCardField({
|
||||||
|
|
@ -96,7 +136,37 @@ export function SwitchCardField({
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
disabled,
|
disabled,
|
||||||
children,
|
children,
|
||||||
|
layout = "default",
|
||||||
}: SwitchCardFieldProps) {
|
}: SwitchCardFieldProps) {
|
||||||
|
if (layout === "setting-row") {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 py-4 md:grid md:grid-cols-[minmax(0,1fr)_auto] md:items-center md:gap-6">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-medium">{label}</p>
|
||||||
|
{hint && (
|
||||||
|
<p className="text-muted-foreground mt-0.5 text-xs leading-normal">
|
||||||
|
{hint}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center md:justify-self-center">
|
||||||
|
<Switch
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label={ariaLabel ?? label}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{children && <div className="md:col-start-2">{children}</div>}
|
||||||
|
{error && (
|
||||||
|
<p className="text-destructive text-xs leading-normal md:col-start-2">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-border/60 bg-background rounded-lg border px-4 py-3">
|
<div className="border-border/60 bg-background rounded-lg border px-4 py-3">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
|
|
||||||
|
|
@ -331,33 +331,10 @@
|
||||||
"pages": {
|
"pages": {
|
||||||
"agent": {
|
"agent": {
|
||||||
"load_error": "Failed to load agent support information.",
|
"load_error": "Failed to load agent support information.",
|
||||||
"stats": {
|
|
||||||
"workspace": "Workspace",
|
|
||||||
"workspace_hint": "The default agent workspace used for runtime files and workspace skills.",
|
|
||||||
"skills": "Available Skills",
|
|
||||||
"skills_hint": "Skills discovered from workspace, global, and builtin roots.",
|
|
||||||
"tools": "Enabled Tools",
|
|
||||||
"tools_hint": "{{blocked}} blocked by missing dependencies."
|
|
||||||
},
|
|
||||||
"skills": {
|
"skills": {
|
||||||
"title": "Skills",
|
|
||||||
"description": "Skills are loaded from the workspace, global PicoClaw home, and builtin directories.",
|
"description": "Skills are loaded from the workspace, global PicoClaw home, and builtin directories.",
|
||||||
"hero_title": "Skill Library",
|
|
||||||
"hero_description": "Browse every capability package the agent can load, then drill straight into the effective SKILL.md without leaving the page.",
|
|
||||||
"stats": {
|
|
||||||
"total": "Total Skills",
|
|
||||||
"workspace": "Workspace",
|
|
||||||
"shared": "Shared"
|
|
||||||
},
|
|
||||||
"empty": "No skills are currently available.",
|
"empty": "No skills are currently available.",
|
||||||
"import": "Import Skill",
|
"import": "Import Skill",
|
||||||
"import_title": "Import Skill",
|
|
||||||
"import_description": "Create a workspace skill by uploading a markdown file as the new SKILL.md.",
|
|
||||||
"import_name": "Skill Name",
|
|
||||||
"import_name_placeholder": "e.g. my-workflow",
|
|
||||||
"import_file": "Markdown File",
|
|
||||||
"import_file_hint": "Upload a .md file. The backend stores it as workspace/skills/<name>/SKILL.md.",
|
|
||||||
"import_confirm": "Import Skill",
|
|
||||||
"import_success": "Skill imported.",
|
"import_success": "Skill imported.",
|
||||||
"import_error": "Failed to import skill.",
|
"import_error": "Failed to import skill.",
|
||||||
"view": "View",
|
"view": "View",
|
||||||
|
|
@ -371,28 +348,11 @@
|
||||||
"viewer_description": "Read the current effective SKILL.md content here.",
|
"viewer_description": "Read the current effective SKILL.md content here.",
|
||||||
"loading_detail": "Loading skill content...",
|
"loading_detail": "Loading skill content...",
|
||||||
"load_detail_error": "Failed to load skill content.",
|
"load_detail_error": "Failed to load skill content.",
|
||||||
"source": "Source",
|
|
||||||
"path": "Skill Path",
|
"path": "Skill Path",
|
||||||
"no_description": "No description provided.",
|
"no_description": "No description provided."
|
||||||
"sources": {
|
|
||||||
"workspace": "Workspace",
|
|
||||||
"global": "Global",
|
|
||||||
"builtin": "Builtin"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"file_required": "Please choose a markdown file to import."
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"title": "Tools",
|
|
||||||
"description": "This view reflects whether each agent tool is enabled, disabled, or blocked by a missing prerequisite.",
|
"description": "This view reflects whether each agent tool is enabled, disabled, or blocked by a missing prerequisite.",
|
||||||
"hero_title": "Tool Surface",
|
|
||||||
"hero_description": "Inspect what the agent can actually call right now, which capabilities are blocked, and where each tool is controlled in config.",
|
|
||||||
"stats": {
|
|
||||||
"enabled": "Enabled",
|
|
||||||
"blocked": "Blocked",
|
|
||||||
"categories": "Categories"
|
|
||||||
},
|
|
||||||
"empty": "No tools are available.",
|
"empty": "No tools are available.",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
|
|
@ -468,13 +428,15 @@
|
||||||
"allowed_cidrs": "Allowed Network CIDRs",
|
"allowed_cidrs": "Allowed Network CIDRs",
|
||||||
"allowed_cidrs_hint": "Only clients from these CIDR ranges can access the service. One per line or comma-separated. Leave empty to allow all.",
|
"allowed_cidrs_hint": "Only clients from these CIDR ranges can access the service. One per line or comma-separated. Leave empty to allow all.",
|
||||||
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
|
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
|
||||||
"launcher_load_error": "Failed to load service parameters.",
|
"sections": {
|
||||||
"launcher_restart_hint": "Service parameter changes apply after restarting PicoClaw Web.",
|
"agent": "Agent",
|
||||||
"advanced_desc": "Open the raw JSON page to edit every field directly.",
|
"runtime": "Runtime",
|
||||||
|
"launcher": "Service",
|
||||||
|
"devices": "Devices"
|
||||||
|
},
|
||||||
"open_raw": "Raw Config",
|
"open_raw": "Raw Config",
|
||||||
"back_to_visual": "Visual Config",
|
"back_to_visual": "Visual Config",
|
||||||
"raw_json_title": "Raw JSON Configuration",
|
"raw_json_title": "Raw JSON Configuration",
|
||||||
"raw_json_desc": "Advanced users can directly edit the raw JSON configuration below.",
|
|
||||||
"json_placeholder": "Enter valid JSON configuration...",
|
"json_placeholder": "Enter valid JSON configuration...",
|
||||||
"save_success": "Configuration saved successfully.",
|
"save_success": "Configuration saved successfully.",
|
||||||
"save_error": "Failed to save configuration.",
|
"save_error": "Failed to save configuration.",
|
||||||
|
|
|
||||||
|
|
@ -331,33 +331,10 @@
|
||||||
"pages": {
|
"pages": {
|
||||||
"agent": {
|
"agent": {
|
||||||
"load_error": "加载 Agent 支持信息失败。",
|
"load_error": "加载 Agent 支持信息失败。",
|
||||||
"stats": {
|
|
||||||
"workspace": "工作目录",
|
|
||||||
"workspace_hint": "默认 Agent 运行时使用的工作目录,也用于加载工作区技能。",
|
|
||||||
"skills": "可用技能数",
|
|
||||||
"skills_hint": "从工作区、全局目录和内置目录发现的技能。",
|
|
||||||
"tools": "已启用工具",
|
|
||||||
"tools_hint": "其中 {{blocked}} 个因依赖未满足而不可用。"
|
|
||||||
},
|
|
||||||
"skills": {
|
"skills": {
|
||||||
"title": "技能",
|
|
||||||
"description": "技能会从工作区、PicoClaw 全局目录和内置目录中加载。",
|
"description": "技能会从工作区、PicoClaw 全局目录和内置目录中加载。",
|
||||||
"hero_title": "技能库",
|
|
||||||
"hero_description": "在这里查看 Agent 当前可加载的能力包,并且不离开页面就能直接阅读生效后的 SKILL.md。",
|
|
||||||
"stats": {
|
|
||||||
"total": "技能总数",
|
|
||||||
"workspace": "工作区技能",
|
|
||||||
"shared": "共享技能"
|
|
||||||
},
|
|
||||||
"empty": "当前没有可用技能。",
|
"empty": "当前没有可用技能。",
|
||||||
"import": "导入技能",
|
"import": "导入技能",
|
||||||
"import_title": "导入技能",
|
|
||||||
"import_description": "通过上传 Markdown 文件创建工作区技能,文件会保存为新的 SKILL.md。",
|
|
||||||
"import_name": "技能名称",
|
|
||||||
"import_name_placeholder": "例如 my-workflow",
|
|
||||||
"import_file": "Markdown 文件",
|
|
||||||
"import_file_hint": "上传一个 .md 文件。后端会保存到 workspace/skills/<name>/SKILL.md。",
|
|
||||||
"import_confirm": "导入技能",
|
|
||||||
"import_success": "技能导入成功。",
|
"import_success": "技能导入成功。",
|
||||||
"import_error": "导入技能失败。",
|
"import_error": "导入技能失败。",
|
||||||
"view": "查看",
|
"view": "查看",
|
||||||
|
|
@ -371,28 +348,11 @@
|
||||||
"viewer_description": "这里展示当前生效的 SKILL.md 内容。",
|
"viewer_description": "这里展示当前生效的 SKILL.md 内容。",
|
||||||
"loading_detail": "正在加载技能内容...",
|
"loading_detail": "正在加载技能内容...",
|
||||||
"load_detail_error": "加载技能内容失败。",
|
"load_detail_error": "加载技能内容失败。",
|
||||||
"source": "来源",
|
|
||||||
"path": "技能路径",
|
"path": "技能路径",
|
||||||
"no_description": "未提供描述。",
|
"no_description": "未提供描述。"
|
||||||
"sources": {
|
|
||||||
"workspace": "工作区",
|
|
||||||
"global": "全局",
|
|
||||||
"builtin": "内置"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"file_required": "请先选择要导入的 Markdown 文件。"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"title": "工具",
|
|
||||||
"description": "这里展示每个 Agent 工具当前是已启用、已禁用,还是被依赖条件阻塞。",
|
"description": "这里展示每个 Agent 工具当前是已启用、已禁用,还是被依赖条件阻塞。",
|
||||||
"hero_title": "工具面板",
|
|
||||||
"hero_description": "集中查看 Agent 现在真正可调用的工具、被阻塞的能力,以及它们分别受哪项配置控制。",
|
|
||||||
"stats": {
|
|
||||||
"enabled": "已启用",
|
|
||||||
"blocked": "被阻塞",
|
|
||||||
"categories": "分类数"
|
|
||||||
},
|
|
||||||
"empty": "当前没有可用工具。",
|
"empty": "当前没有可用工具。",
|
||||||
"enable": "启用",
|
"enable": "启用",
|
||||||
"disable": "禁用",
|
"disable": "禁用",
|
||||||
|
|
@ -465,16 +425,18 @@
|
||||||
"server_port_hint": "PicoClaw Web 的 HTTP 监听端口。",
|
"server_port_hint": "PicoClaw Web 的 HTTP 监听端口。",
|
||||||
"lan_access": "启用局域网访问",
|
"lan_access": "启用局域网访问",
|
||||||
"lan_access_hint": "允许局域网中的其他设备访问当前服务。",
|
"lan_access_hint": "允许局域网中的其他设备访问当前服务。",
|
||||||
"allowed_cidrs": "允许访问网段(CIDR)",
|
"allowed_cidrs": "允许访问网段",
|
||||||
"allowed_cidrs_hint": "仅允许这些 CIDR 网段的客户端访问服务。可按行或逗号分隔;留空表示允许所有来源。",
|
"allowed_cidrs_hint": "仅允许这些 CIDR 网段的客户端访问服务。可按行或逗号分隔;留空表示允许所有来源。",
|
||||||
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
|
"allowed_cidrs_placeholder": "192.168.1.0/24\n10.0.0.0/8",
|
||||||
"launcher_load_error": "加载服务参数失败。",
|
"sections": {
|
||||||
"launcher_restart_hint": "服务参数变更需重启 PicoClaw Web 后生效。",
|
"agent": "智能体",
|
||||||
"advanced_desc": "可打开原始 JSON 页面直接编辑全部字段。",
|
"runtime": "运行时",
|
||||||
|
"launcher": "服务参数",
|
||||||
|
"devices": "设备"
|
||||||
|
},
|
||||||
"open_raw": "原始配置",
|
"open_raw": "原始配置",
|
||||||
"back_to_visual": "可视化配置",
|
"back_to_visual": "可视化配置",
|
||||||
"raw_json_title": "原始 JSON 配置",
|
"raw_json_title": "原始 JSON 配置",
|
||||||
"raw_json_desc": "高级用户可以直接编辑下方的原始 JSON 配置。",
|
|
||||||
"json_placeholder": "请输入有效的 JSON 配置...",
|
"json_placeholder": "请输入有效的 JSON 配置...",
|
||||||
"save_success": "配置保存成功。",
|
"save_success": "配置保存成功。",
|
||||||
"save_error": "配置保存失败。",
|
"save_error": "配置保存失败。",
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,7 @@
|
||||||
import { IconAdjustments } from "@tabler/icons-react"
|
import { createFileRoute } from "@tanstack/react-router"
|
||||||
import { Link, createFileRoute } from "@tanstack/react-router"
|
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
|
|
||||||
import { RawJsonPanel } from "@/components/config/raw-json-panel"
|
import { RawConfigPage } from "@/components/config/raw-config-page"
|
||||||
import { PageHeader } from "@/components/page-header"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
|
|
||||||
export const Route = createFileRoute("/config/raw")({
|
export const Route = createFileRoute("/config/raw")({
|
||||||
component: RawConfigPage,
|
component: RawConfigPage,
|
||||||
})
|
})
|
||||||
|
|
||||||
function RawConfigPage() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full flex-col">
|
|
||||||
<PageHeader title={t("pages.config.raw_json_title")}>
|
|
||||||
<Button variant="outline" asChild>
|
|
||||||
<Link to="/config">
|
|
||||||
<IconAdjustments className="size-4" />
|
|
||||||
{t("pages.config.back_to_visual")}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto p-3 lg:p-6">
|
|
||||||
<div className="mx-auto max-w-4xl">
|
|
||||||
<RawJsonPanel />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ Default model is `google/gemini-3-flash-preview` if none is set.
|
||||||
Optional config file: `~/.summarize/config.json`
|
Optional config file: `~/.summarize/config.json`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{ "model": "openai/gpt-5.2" }
|
{ "model": "openai/gpt-5.4" }
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional services:
|
Optional services:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue