feat: add LuCI web management interface for OpenWrt

This commit is contained in:
GennKann 2026-03-28 21:46:09 +08:00
parent 1809d04905
commit 2f1b196aea
15 changed files with 2414 additions and 0 deletions

View file

@ -0,0 +1,66 @@
MIT License
Copyright (c) 2026 GennKann
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## Disclaimer & Third-Party Notice
### Relationship to PicoClaw
This project (**luci-app-picoclaw**) is an **independently created** LuCI web
management interface designed for OpenWrt routers. It provides a convenient
graphical frontend to manage the [PicoClaw](https://github.com/sipeed/picoclaw)
service via the OpenWrt LuCI web interface.
**This project is NOT a derivative work of PicoClaw.** All source code in this
repository (Lua controller, HTML template, installation scripts, init.d service
file) was originally written by the author and is licensed under the MIT License
above. No source code from PicoClaw is included, copied, or modified in this
repository.
### What This Project Does
- Provides a LuCI web UI (controller + view) for managing the PicoClaw service
- Includes installation scripts and init.d service configuration for OpenWrt
- Communicates with PicoClaw via its standard CLI/API interface only
### PicoClaw License
PicoClaw is developed by [Sipeed](https://www.sipeed.com/) and is licensed under
the MIT License. If you use this management interface, you must also comply with
the PicoClaw license terms when using PicoClaw itself.
- PicoClaw repository: https://github.com/sipeed/picoclaw
- PicoClaw license: https://github.com/sipeed/picoclaw/blob/main/LICENSE
### Trademark Notice
"PicoClaw" and "Sipeed" are trademarks of their respective owners.
This project is **not affiliated with, endorsed by, or sponsored by** Sipeed
or the PicoClaw project.
### No Warranty
This management interface is provided as-is. The author makes no guarantees
about its compatibility with current or future versions of PicoClaw or OpenWrt.
Users are responsible for ensuring that their use of PicoClaw complies with
PicoClaw's own license terms.

View file

@ -0,0 +1,161 @@
<p align="center">
<img src="Screenshots/logo.svg" alt="PicoClaw LuCI" width="120">
</p>
<h1 align="center">PicoClaw LuCI — OpenWrt Webverwaltung</h1>
<p align="center">
<b>Eine schöne LuCI-Weboberfläche zur Verwaltung von <a href="https://github.com/sipeed/picoclaw">PicoClaw</a> auf OpenWrt-Routern</b>
</p>
<p align="center">
<img src="https://img.shields.io/badge/OpenWrt-24.10%20%7C%2025.xx-blue?logo=openwrt" alt="OpenWrt">
<img src="https://img.shields.io/badge/LuCI-Web%20Interface-green?logo=lua" alt="LuCI">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
<img src="https://img.shields.io/badge/Arch-ARM64%20%7C%20AMD64%20%7C%20ARMv7-orange" alt="Architecture">
</p>
<p align="center">
<a href="README.md">English</a> ·
<a href="README.zh.md">简体中文</a> ·
<a href="README.ja.md">日本語</a> ·
<a href="README.de.md">Deutsch</a> ·
<a href="README.pt.md">Português</a>
</p>
---
## 📸 Screenshots
| Desktop | Mobil |
|:---:|:---:|
| ![Dashboard](Screenshots/screenshot_main.png) | ![Mobil](Screenshots/screenshot_mobile.png) |
| ![Konfiguration](Screenshots/screenshot_config.png) | — |
## ✨ Funktionen
- 🖥️ **Modernes Dashboard** — Echtzeit-Service-Status, PID, Speichernutzung und Port-Überwachung
- 🚀 **Service-Steuerung** — PicoClaw mit einem Klick starten / stoppen / neu starten
- ⚡ **Autostart-Umschalter** — Autostart direkt in der Weboberfläche aktivieren/deaktivieren
- 📡 **Kanalverwaltung** — Alle Kanäle anzeigen und verwalten (Feishu, Telegram, Discord, WeChat, WeCom, Slack, QQ, LINE, DingTalk, WhatsApp, MaixCam)
- 🔧 **Formular-Konfiguration** — Intuitive Formular-Benutzeroberfläche für AI-Modell, Anbieter, Systemeinstellungen und Tools
- 📝 **JSON-Editor** — Direkte JSON-Bearbeitung mit Formatierung und Validierung
- 🌍 **5 Sprachen** — Eingebauter i18n: English, 简体中文, 日本語, Português, Deutsch
- 🔄 **Online-Update** — Neue Versionen prüfen und direkt aktualisieren
- 🔑 **Multi-Anbieter** — API-Schlüssel für Zhipu, OpenAI, ChatGPT, Claude, DeepSeek, Anthropic, Ollama, Azure OpenAI
## 📋 Voraussetzungen
| Anforderung | Details |
|---|---|
| **OpenWrt** | 24.10 / 25.xx (mit LuCI) |
| **Architektur** | ARM64, AMD64, ARMv7 |
| **SSH** | Für das Installationsskript erforderlich |
| **Python** | 3.6+ (auf dem PC) |
| **PicoClaw** | [sipeed/picoclaw](https://github.com/sipeed/picoclaw) neueste Version |
## 🚀 Installation
### Methode 1: Python-Installer (Empfohlen)
```bash
pip install paramiko
wget https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/install_picoclaw_luci.py
python install_picoclaw_luci.py
```
### Methode 2: Paket-Installation
> **Hinweis:** OpenWrt 24.10 verwendet `.ipk` (opkg), OpenWrt 25.xx verwendet `.apk` (apk-tools). Dies sind **unterschiedliche** Paketformate.
#### OpenWrt 24.10opkg / .ipk
```bash
opkg install luci-compat
wget -O /tmp/luci-app-picoclaw.ipk https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/luci-app-picoclaw_24.10_all.ipk
opkg install /tmp/luci-app-picoclaw.ipk
```
#### OpenWrt 25.xxapk / .apk
OpenWrt 25.x hat den Paketmanager von `opkg` auf `apk-tools` umgestellt. Das Format änderte sich von `.ipk` zu `.apk`. Bauen Sie das Paket direkt auf dem Router:
```bash
# Build-Skript herunterladen
wget -O /tmp/build-apk-25xx.sh https://raw.githubusercontent.com/GennKann/luci-app-picoclaw/main/scripts/build-apk-25xx.sh
chmod +x /tmp/build-apk-25xx.sh
# .apk-Paket bauen
/tmp/build-apk-25xx.sh
# Generiertes Paket installieren
apk add --allow-untrusted /root/luci-app-picoclaw_1.0.0_all.apk
```
### Methode 3: Manuelle Installation
```bash
# PicoClaw installieren
ARCH=$(uname -m)
if echo "$ARCH" | grep -q "x86"; then PLAT="linux_amd64";
elif echo "$ARCH" | grep -q "armv7"; then PLAT="linux_armv7";
else PLAT="linux_arm64"; fi
wget -O /tmp/picoclaw "https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_${PLAT}"
chmod +x /tmp/picoclaw && mv /tmp/picoclaw /usr/bin/picoclaw
# LuCI-Dateien und init.d Service einrichten
# (Dateien aus diesem Repository kopieren)
```
## 🎯 Zugriff
```
http://<ROUTER-IP>/cgi-bin/luci/admin/services/picoclaw
```
## ⚠️ Haftungsausschluss
Dieses Projekt ist eine Community-getriebene LuCI-Verwaltungsoberfläche für PicoClaw und ist **kein** Teil des offiziellen PicoClaw-Projekts.
- **PicoClaw** wird von [Sipeed](https://github.com/sipeed) unter der MIT-Lizenz entwickelt
- Diese LuCI-Oberfläche ist ein unabhängiges Open-Source-Tool
- „PicoClaw" und „Sipeed" sind Marken ihrer jeweiligen Eigentümer
## 📄 Lizenz
[MIT-Lizenz](LICENSE)
## 🙏 Danksagungen
- [PicoClaw](https://github.com/sipeed/picoclaw) by Sipeed — Der großartige KI-Assistent, den dieses Projekt verwaltet
- [OpenWrt](https://openwrt.org/) — Das Fundament, auf dem dies läuft
- [LuCI](https://github.com/openwrt/luci) — Das Web-Interface-Framework
## 🤝 Mitmachen
Beiträge sind willkommen! Bitte erstellen Sie einen Pull Request.
1. Forken Sie das Repository
2. Erstellen Sie einen Feature-Branch (`git checkout -b feature/amazing-feature`)
3. Committen Sie Ihre Änderungen (`git commit -m 'Add amazing feature'`)
4. Pushen Sie den Branch (`git push origin feature/amazing-feature`)
5. Erstellen Sie einen Pull Request
---
<p align="center">
Wenn Sie dieses Projekt nützlich finden, geben Sie ihm bitte einen ⭐!
</p>
<p align="center">
<a href="https://github.com/GennKann/luci-app-picoclaw">
<img src="https://img.shields.io/github/stars/GennKann/luci-app-picoclaw?style=social" alt="Stars">
</a>
<a href="https://github.com/GennKann/luci-app-picoclaw/fork">
<img src="https://img.shields.io/github/forks/GennKann/luci-app-picoclaw?style=social" alt="Forks">
</a>
</p>
<p align="center">
<b>Made with ❤️ by <a href="https://github.com/GennKann">GennKann</a></b>
</p>

View file

@ -0,0 +1,172 @@
<p align="center">
<img src="Screenshots/logo.svg" alt="PicoClaw LuCI" width="120">
</p>
<h1 align="center">PicoClaw LuCI — OpenWrt Web管理インターフェース</h1>
<p align="center">
<b><a href="https://github.com/sipeed/picoclaw">PicoClaw</a> 専用 OpenWrt LuCI 美しい管理パネル</b>
</p>
<p align="center">
<img src="https://img.shields.io/badge/OpenWrt-24.10%20%7C%2025.xx-blue?logo=openwrt" alt="OpenWrt">
<img src="https://img.shields.io/badge/LuCI-Web%20Interface-green?logo=lua" alt="LuCI">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
<img src="https://img.shields.io/badge/Arch-ARM64%20%7C%20AMD64%20%7C%20ARMv7-orange" alt="Architecture">
</p>
<p align="center">
<a href="README.md">English</a> ·
<a href="README.zh.md">简体中文</a> ·
<a href="README.ja.md">日本語</a> ·
<a href="README.de.md">Deutsch</a> ·
<a href="README.pt.md">Português</a>
</p>
---
## 📸 スクリーンショット
| デスクトップ | モバイル |
|:---:|:---:|
| ![ダッシュボード](Screenshots/screenshot_main.png) | ![モバイル](Screenshots/screenshot_mobile.png) |
| ![設定エディタ](Screenshots/screenshot_config.png) | — |
## ✨ 機能
- 🖥️ **モダンダッシュボード** — サービス状態、PID、メモリ使用量、ポート監視をリアルタイム表示
- 🚀 **サービス制御** — PicoClawの開始/停止/再起動をワンクリック
- ⚡ **自動起動切替** — Web UIから起動時自動開始の有効/無効を切り替え
- 📡 **チャンネル管理** — 全チャンネルの表示・管理Feishu、Telegram、Discord、WeChat、WeCom、Slack、QQ、LINE、DingTalk、WhatsApp、MaixCam
- 🔧 **フォーム設定エディタ** — 直感的なフォームUIでAIモデル、プロバイダー、システム設定、ツールを設定
- 📝 **JSON設定エディタ** — JSON直接編集、フォーマット、バリデーション対応
- 🌍 **5言語対応** — 組み込みi18nEnglish、简体中文、日本語、Português、Deutsch
- 🔄 **オンライン更新** — 新バージョンの確認とUIからの直接更新
- 📊 **システムログ** — PicoClawシステムログをリアルタイム表示
- 🔑 **マルチプロバイダー対応** — Zhipu、OpenAI、ChatGPT、Claude、DeepSeek、Anthropic、Ollama、Azure OpenAIのAPIキー設定
- 🎨 **レスポンシブデザイン** — デスクトップとモバイルブラウザの両方に対応
## 📋 動作要件
| 要件 | 詳細 |
|---|---|
| **OpenWrt** | 24.10 / 25.xxLuCI導入済み |
| **アーキテクチャ** | ARM64、AMD64 (x86_64)、ARMv7 |
| **LuCI** | 標準LuCIインストール |
| **SSH** | インストーラースクリプトに必要 |
| **Python** | 3.6+PC環境 |
| **PicoClaw** | [sipeed/picoclaw](https://github.com/sipeed/picoclaw) 最新版 |
## 🚀 インストール
### 方法1ワンクリックPythonインストーラー推奨
PC上で実行 — SSH経由でルーターに自動インストールします。
```bash
pip install paramiko
wget https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/install_picoclaw_luci.py
python install_picoclaw_luci.py
```
### 方法2パッケージインストール
> **注意:** OpenWrt 24.10は`.ipk`opkg、OpenWrt 25.xxは`.apk`apk-toolsを使用します。**別の**パッケージ形式です。
#### OpenWrt 24.10opkg / .ipk
```bash
opkg install luci-compat
wget -O /tmp/luci-app-picoclaw.ipk https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/luci-app-picoclaw_24.10_all.ipk
opkg install /tmp/luci-app-picoclaw.ipk
```
#### OpenWrt 25.xxapk / .apk
OpenWrt 25.xではパッケージマネージャーが`opkg`から`apk-tools`に変更され、形式も`.ipk`から`.apk`になりました。ルーター上で直接ビルドします:
```bash
# ビルドスクリプトをダウンロード
wget -O /tmp/build-apk-25xx.sh https://raw.githubusercontent.com/GennKann/luci-app-picoclaw/main/scripts/build-apk-25xx.sh
chmod +x /tmp/build-apk-25xx.sh
# .apkパッケージをビルド
/tmp/build-apk-25xx.sh
# 生成されたパッケージをインストール
apk add --allow-untrusted /root/luci-app-picoclaw_1.0.0_all.apk
```
### 方法3手動インストール
```bash
# PicoClawインストール
ARCH=$(uname -m)
if echo "$ARCH" | grep -q "x86"; then PLAT="linux_amd64";
elif echo "$ARCH" | grep -q "armv7"; then PLAT="linux_armv7";
else PLAT="linux_arm64"; fi
wget -O /tmp/picoclaw "https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_${PLAT}"
chmod +x /tmp/picoclaw && mv /tmp/picoclaw /usr/bin/picoclaw
# LuCIファイル配置
# controller → /usr/lib/lua/luci/controller/picoclaw.lua
# template → /usr/lib/lua/luci/view/picoclaw/main.htm
# init.dサービス設定
cp scripts/picoclaw.init /etc/init.d/picoclaw
chmod +x /etc/init.d/picoclaw && /etc/init.d/picoclaw enable
rm -rf /tmp/luci-* && /etc/init.d/picoclaw start
```
## 🎯 アクセス
```
http://<ルーターIP>/cgi-bin/luci/admin/services/picoclaw
```
## ⚠️ 免責事項
このプロジェクトはコミュニティ駆動のPicoClaw管理インターフェースであり、PicoClaw公式プロジェクトの一部ではありません。
- **PicoClaw**は[Sipeed](https://github.com/sipeed)によってMITライセンスで開発されています
- 本LuCIインターフェースは独立したオープンソースツールです
- "PicoClaw"および"Sipeed"は各所有者の商標です
## 📄 ライセンス
[MIT License](LICENSE)
## 🙏 クレジット
- [PicoClaw](https://github.com/sipeed/picoclaw) by Sipeed — このプロジェクトが管理する素晴らしいAIアシスタント
- [OpenWrt](https://openwrt.org/) — 実行環境
- [LuCI](https://github.com/openwrt/luci) — Webインターフェースフレームワーク
## 🤝 コントリビューション
コントリビューションをお待ちしていますお気軽にPull Requestをどうぞ。
1. リポジトリをフォーク
2. フィーチャーブランチを作成 (`git checkout -b feature/amazing-feature`)
3. 変更をコミット (`git commit -m 'Add amazing feature'`)
4. ブランチをプッシュ (`git push origin feature/amazing-feature`)
5. Pull Requestを作成
---
<p align="center">
このプロジェクトが役立つと思ったら、⭐ をお願いします!
</p>
<p align="center">
<a href="https://github.com/GennKann/luci-app-picoclaw">
<img src="https://img.shields.io/github/stars/GennKann/luci-app-picoclaw?style=social" alt="Stars">
</a>
<a href="https://github.com/GennKann/luci-app-picoclaw/fork">
<img src="https://img.shields.io/github/forks/GennKann/luci-app-picoclaw?style=social" alt="Forks">
</a>
</p>
<p align="center">
<b>Made with ❤️ by <a href="https://github.com/GennKann">GennKann</a></b>
</p>

View file

@ -0,0 +1,69 @@
# LuCI Management Interface for PicoClaw on OpenWrt
A beautiful LuCI web interface to manage PicoClaw on OpenWrt routers.
## Features
- **Modern Dashboard** — Real-time service status, PID, memory usage, and port monitoring
- **Service Control** — Start / Stop / Restart PicoClaw with one click
- **Auto-Start Toggle** — Enable or disable boot-on-start from the web UI
- **Channel Management** — View and manage all connected channels
- **Form-based Config Editor** — Intuitive form UI for AI model, providers, system settings, and tools configuration
- **JSON Config Editor** — Direct JSON editing with format and validation
- **5 Languages** — Built-in i18n: English, 简体中文, 日本語, Português, Deutsch
- **Online Update** — Check for new versions and update directly from the UI
- **System Logs** — View PicoClaw system logs in real-time
- **Responsive Design** — Works on both desktop and mobile browsers
## Installation
### One-Click Python Installer (Recommended)
```bash
pip install paramiko
python contrib/luci-app-picoclaw/install.py
```
### Manual Installation
```bash
# Copy LuCI controller
cp contrib/luci-app-picoclaw/luci/controller/picoclaw.lua /usr/lib/lua/luci/controller/
# Copy LuCI template
mkdir -p /usr/lib/lua/luci/view/picoclaw
cp contrib/luci-app-picoclaw/luci/view/picoclaw/main.htm /usr/lib/lua/luci/view/picoclaw/
# Set up init.d service
cp contrib/luci-app-picoclaw/scripts/picoclaw.init /etc/init.d/picoclaw
chmod +x /etc/init.d/picoclaw
/etc/init.d/picoclaw enable
```
## Requirements
- OpenWrt 24.10 / 25.xx with LuCI installed
- ARM64 / AMD64 / ARMv7
- PicoClaw latest version
## Screenshots
| Desktop | Mobile |
|:---:|:---:|
| ![Dashboard](Screenshots/screenshot_main.png) | ![Mobile](Screenshots/screenshot_mobile.png) |
| ![Config](Screenshots/screenshot_config.png) | — |
## Access
```
http://<ROUTER_IP>/cgi-bin/luci/admin/services/picoclaw
```
## License
MIT License — This is an independently created LuCI management interface.
It communicates with PicoClaw via its standard CLI/API interface only.
## Links
- Standalone repo: [GennKann/luci-app-picoclaw](https://github.com/GennKann/luci-app-picoclaw)

View file

@ -0,0 +1,161 @@
<p align="center">
<img src="Screenshots/logo.svg" alt="PicoClaw LuCI" width="120">
</p>
<h1 align="center">PicoClaw LuCI — Gerenciador Web OpenWrt</h1>
<p align="center">
<b>Uma interface web LuCI bonita para gerenciar o <a href="https://github.com/sipeed/picoclaw">PicoClaw</a> em roteadores OpenWrt</b>
</p>
<p align="center">
<img src="https://img.shields.io/badge/OpenWrt-24.10%20%7C%2025.xx-blue?logo=openwrt" alt="OpenWrt">
<img src="https://img.shields.io/badge/LuCI-Web%20Interface-green?logo=lua" alt="LuCI">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
<img src="https://img.shields.io/badge/Arch-ARM64%20%7C%20AMD64%20%7C%20ARMv7-orange" alt="Architecture">
</p>
<p align="center">
<a href="README.md">English</a> ·
<a href="README.zh.md">简体中文</a> ·
<a href="README.ja.md">日本語</a> ·
<a href="README.de.md">Deutsch</a> ·
<a href="README.pt.md">Português</a>
</p>
---
## 📸 Capturas de Tela
| Desktop | Mobile |
|:---:|:---:|
| ![Painel](Screenshots/screenshot_main.png) | ![Mobile](Screenshots/screenshot_mobile.png) |
| ![Configuração](Screenshots/screenshot_config.png) | — |
## ✨ Funcionalidades
- 🖥️ **Painel Moderno** — Status do serviço em tempo real, PID, uso de memória e monitoramento de porta
- 🚀 **Controle de Serviço** — Iniciar / Parar / Reiniciar o PicoClaw com um clique
- ⚡ **Alternar Auto-início** — Ativar/desativar inicialização automática pela interface web
- 📡 **Gerenciamento de Canais** — Visualizar e gerenciar todos os canais (Feishu, Telegram, Discord, WeChat, WeCom, Slack, QQ, LINE, DingTalk, WhatsApp, MaixCam)
- 🔧 **Editor de Configuração** — Interface de formulário intuitiva para modelo AI, provedores, configurações do sistema e ferramentas
- 📝 **Editor JSON** — Edição direta JSON com formatação e validação
- 🌍 **5 Idiomas** — i18n integrado: English, 简体中文, 日本語, Português, Deutsch
- 🔄 **Atualização Online** — Verificar novas versões e atualizar diretamente pela interface
- 🔑 **Multi-provedor** — Chaves API para Zhipu, OpenAI, ChatGPT, Claude, DeepSeek, Anthropic, Ollama, Azure OpenAI
## 📋 Requisitos
| Requisito | Detalhes |
|---|---|
| **OpenWrt** | 24.10 / 25.xx (com LuCI) |
| **Arquitetura** | ARM64, AMD64, ARMv7 |
| **SSH** | Necessário para o script de instalação |
| **Python** | 3.6+ (no PC) |
| **PicoClaw** | [sipeed/picoclaw](https://github.com/sipeed/picoclaw) versão mais recente |
## 🚀 Instalação
### Método 1: Instalador Python (Recomendado)
```bash
pip install paramiko
wget https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/install_picoclaw_luci.py
python install_picoclaw_luci.py
```
### Método 2: Instalação por Pacote
> **Nota:** OpenWrt 24.10 usa `.ipk` (opkg), OpenWrt 25.xx usa `.apk` (apk-tools). São **formatos diferentes** de pacotes.
#### OpenWrt 24.10opkg / .ipk
```bash
opkg install luci-compat
wget -O /tmp/luci-app-picoclaw.ipk https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/luci-app-picoclaw_24.10_all.ipk
opkg install /tmp/luci-app-picoclaw.ipk
```
#### OpenWrt 25.xxapk / .apk
OpenWrt 25.x mudou o gerenciador de pacotes de `opkg` para `apk-tools`, e o formato de `.ipk` para `.apk`. Construa o pacote diretamente no roteador:
```bash
# Baixar script de compilação
wget -O /tmp/build-apk-25xx.sh https://raw.githubusercontent.com/GennKann/luci-app-picoclaw/main/scripts/build-apk-25xx.sh
chmod +x /tmp/build-apk-25xx.sh
# Compilar pacote .apk
/tmp/build-apk-25xx.sh
# Instalar pacote gerado
apk add --allow-untrusted /root/luci-app-picoclaw_1.0.0_all.apk
```
### Método 3: Instalação Manual
```bash
# Instalar PicoClaw
ARCH=$(uname -m)
if echo "$ARCH" | grep -q "x86"; then PLAT="linux_amd64";
elif echo "$ARCH" | grep -q "armv7"; then PLAT="linux_armv7";
else PLAT="linux_arm64"; fi
wget -O /tmp/picoclaw "https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_${PLAT}"
chmod +x /tmp/picoclaw && mv /tmp/picoclaw /usr/bin/picoclaw
# Configurar arquivos LuCI e serviço init.d
# (copiar arquivos deste repositório)
```
## 🎯 Acesso
```
http://<IP_DO_ROTEADOR>/cgi-bin/luci/admin/services/picoclaw
```
## ⚠️ Aviso Legal
Este projeto é uma interface LuCI de gerenciamento do PicoClaw desenvolvida pela comunidade e **NÃO** faz parte do projeto oficial PicoClaw.
- **PicoClaw** é desenvolvido pela [Sipeed](https://github.com/sipeed) sob a licença MIT
- Esta interface LuCI é uma ferramenta open-source independente
- "PicoClaw" e "Sipeed" são marcas registradas de seus respectivos proprietários
## 📄 Licença
[Licença MIT](LICENSE)
## 🙏 Créditos
- [PicoClaw](https://github.com/sipeed/picoclaw) by Sipeed — O excelente assistente de IA que este projeto gerencia
- [OpenWrt](https://openwrt.org/) — A base sobre a qual isso é executado
- [LuCI](https://github.com/openwrt/luci) — O framework de interface web
## 🤝 Contribuição
Contribuições são bem-vindas! Sinta-se à vontade para enviar um Pull Request.
1. Faça um Fork do repositório
2. Crie um branch de funcionalidade (`git checkout -b feature/amazing-feature`)
3. Faça commit das alterações (`git commit -m 'Add amazing feature'`)
4. Faça push do branch (`git push origin feature/amazing-feature`)
5. Abra um Pull Request
---
<p align="center">
Se você achar este projeto útil, por favor, dê uma ⭐!
</p>
<p align="center">
<a href="https://github.com/GennKann/luci-app-picoclaw">
<img src="https://img.shields.io/github/stars/GennKann/luci-app-picoclaw?style=social" alt="Stars">
</a>
<a href="https://github.com/GennKann/luci-app-picoclaw/fork">
<img src="https://img.shields.io/github/forks/GennKann/luci-app-picoclaw?style=social" alt="Forks">
</a>
</p>
<p align="center">
<b>Made with ❤️ by <a href="https://github.com/GennKann">GennKann</a></b>
</p>

View file

@ -0,0 +1,199 @@
<p align="center">
<img src="Screenshots/logo.svg" alt="PicoClaw LuCI" width="120">
</p>
<h1 align="center">PicoClaw LuCI — OpenWrt 网页管理界面</h1>
<p align="center">
<b><a href="https://github.com/sipeed/picoclaw">PicoClaw</a> 量身打造的 OpenWrt LuCI 美化管理面板</b>
</p>
<p align="center">
<img src="https://img.shields.io/badge/OpenWrt-24.10%20%7C%2025.xx-blue?logo=openwrt" alt="OpenWrt">
<img src="https://img.shields.io/badge/LuCI-Web%20Interface-green?logo=lua" alt="LuCI">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
<img src="https://img.shields.io/badge/Arch-ARM64%20%7C%20AMD64%20%7C%20ARMv7-orange" alt="Architecture">
</p>
<p align="center">
<a href="README.md">English</a> ·
<a href="README.zh.md">简体中文</a> ·
<a href="README.ja.md">日本語</a> ·
<a href="README.de.md">Deutsch</a> ·
<a href="README.pt.md">Português</a>
</p>
---
## 📸 截图预览
| 桌面端 | 移动端 |
|:---:|:---:|
| ![主界面](Screenshots/screenshot_main.png) | ![手机端](Screenshots/screenshot_mobile.png) |
| ![配置编辑](Screenshots/screenshot_config.png) | — |
## ✨ 功能特性
- 🖥️ **现代化仪表盘** — 实时显示服务状态、PID、内存占用、端口监听
- 🚀 **服务控制** — 一键启动 / 停止 / 重启 PicoClaw
- ⚡ **开机自启开关** — 在网页上直接控制开机自动启动
- 📡 **通道管理** — 查看和管理所有通道飞书、Telegram、Discord、微信、企业微信、Slack、QQ、LINE、钉钉、WhatsApp、MaixCam
- 🔧 **表单化配置编辑** — 直观的表单界面配置 AI 模型、API 密钥、系统设置和工具
- 📝 **JSON 配置编辑** — 支持 JSON 直接编辑、格式化和验证
- 🌍 **5 种语言** — 内置国际化简体中文、English、日本語、Português、Deutsch
- 🔄 **在线更新** — 检查新版本并直接在界面上更新
- 📊 **系统日志** — 实时查看 PicoClaw 系统日志
- 💬 **微信接入引导** — 微信个人号接入步骤引导
- 🔑 **多 AI 供应商支持** — 配置智谱、OpenAI、ChatGPT、Claude、DeepSeek、Anthropic、Ollama、Azure OpenAI
- 🎨 **响应式设计** — 完美适配桌面和手机浏览器
- 📦 **一键安装** — Python 安装脚本,通过 SSH 自动部署到任意 OpenWrt 设备
## 📋 系统要求
| 要求 | 说明 |
|---|---|
| **OpenWrt** | 24.10 / 25.xx需安装 LuCI |
| **架构** | ARM64、AMD64 (x86_64)、ARMv7 |
| **LuCI** | 标准 LuCI 安装即可 |
| **SSH** | 安装脚本需要路由器开启 SSH |
| **Python** | 3.6+(在电脑上运行安装脚本) |
| **PicoClaw** | [sipeed/picoclaw](https://github.com/sipeed/picoclaw) 最新版 |
## 🚀 安装方式
### 方式一:一键 Python 安装(推荐)
在你的电脑上运行,脚本会通过 SSH 自动完成所有操作。
```bash
# 安装依赖
pip install paramiko
# 下载安装脚本
wget https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/install_picoclaw_luci.py
# 运行 — 按提示输入路由器信息
python install_picoclaw_luci.py
```
安装脚本会自动完成:
1. ✅ 检测系统架构ARM64 / AMD64 / ARMv7
2. ✅ 下载并安装最新版 PicoClaw
3. ✅ 创建 procd init.d 开机自启服务
4. ✅ 部署 LuCI 网页管理界面
5. ✅ 初始化 PicoClaw 配置
6. ✅ 启动服务并验证
### 方式二:手动安装
SSH 登录你的 OpenWrt 路由器:
```bash
# 1. 下载并安装 PicoClaw
ARCH=$(uname -m)
if echo "$ARCH" | grep -q "x86"; then PLAT="linux_amd64";
elif echo "$ARCH" | grep -q "armv7"; then PLAT="linux_armv7";
else PLAT="linux_arm64"; fi
wget -O /tmp/picoclaw "https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_${PLAT}"
chmod +x /tmp/picoclaw && mv /tmp/picoclaw /usr/bin/picoclaw
# 2. 部署 LuCI 文件(从本仓库复制)
# controller → /usr/lib/lua/luci/controller/picoclaw.lua
# template → /usr/lib/lua/luci/view/picoclaw/main.htm
# 3. 设置 init.d 服务
cp scripts/picoclaw.init /etc/init.d/picoclaw
chmod +x /etc/init.d/picoclaw && /etc/init.d/picoclaw enable
# 4. 清理缓存并启动
rm -rf /tmp/luci-* && /etc/init.d/picoclaw start
```
### 方式三:包管理器安装
> **注意:** OpenWrt 24.10 使用 `.ipk`opkg 包管理器OpenWrt 25.xx 使用 `.apk`apk-tools 包管理器),两者是**不同的**包格式。
#### OpenWrt 24.10opkg / .ipk
```bash
opkg install luci-compat
wget -O /tmp/luci-app-picoclaw.ipk https://github.com/GennKann/luci-app-picoclaw/releases/latest/download/luci-app-picoclaw_24.10_all.ipk
opkg install /tmp/luci-app-picoclaw.ipk
```
#### OpenWrt 25.xxapk / .apk
OpenWrt 25.x 已将包管理器从 `opkg` 切换为 `apk-tools`,包格式从 `.ipk` 变为 `.apk`。需要在路由器上直接构建:
```bash
# 下载构建脚本
wget -O /tmp/build-apk-25xx.sh https://raw.githubusercontent.com/GennKann/luci-app-picoclaw/main/scripts/build-apk-25xx.sh
chmod +x /tmp/build-apk-25xx.sh
# 运行构建 .apk 包
/tmp/build-apk-25xx.sh
# 安装生成的包
apk add --allow-untrusted /root/luci-app-picoclaw_1.0.0_all.apk
```
> **为什么需要在路由器上构建?** OpenWrt 25.xx 使用 APKv3Alpine Package Keeper格式需要 `apk-tools` 来正确构建。在路由器上构建可确保完全兼容。
## 🎯 访问地址
安装完成后,通过以下地址访问管理界面:
```
http://<路由器IP>/cgi-bin/luci/admin/services/picoclaw
```
例如:`http://192.168.1.1/cgi-bin/luci/admin/services/picoclaw`
## ⚠️ 免责声明
本项目是社区驱动的 PicoClaw LuCI 管理界面,**不是** PicoClaw 官方项目的一部分。
- **PicoClaw** 由 [Sipeed](https://github.com/sipeed) 使用 MIT 许可证开发
- 本 LuCI 界面是独立的开源工具
- 我们尊重原始项目的所有知识产权
- "PicoClaw" 和 "Sipeed" 是其各自所有者的商标
## 📄 许可证
本项目使用 [MIT 许可证](LICENSE)。
## 🙏 鸣谢
- [PicoClaw](https://github.com/sipeed/picoclaw) by Sipeed — 本项目所管理的出色 AI 助手
- [OpenWrt](https://openwrt.org/) — 运行基础
- [LuCI](https://github.com/openwrt/luci) — Web 界面框架
## 🤝 参与贡献
欢迎贡献!请随时提交 Pull Request。
1. Fork 本仓库
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add amazing feature'`)
4. 推送分支 (`git push origin feature/amazing-feature`)
5. 发起 Pull Request
---
<p align="center">
如果这个项目对你有帮助,请给一个 ⭐ 支持一下!
</p>
<p align="center">
<a href="https://github.com/GennKann/luci-app-picoclaw">
<img src="https://img.shields.io/github/stars/GennKann/luci-app-picoclaw?style=social" alt="Stars">
</a>
<a href="https://github.com/GennKann/luci-app-picoclaw/fork">
<img src="https://img.shields.io/github/forks/GennKann/luci-app-picoclaw?style=social" alt="Forks">
</a>
</p>
<p align="center">
<b>❤️ 由 <a href="https://github.com/GennKann">GennKann</a> 用心制作</b>
</p>

View file

@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e88e5;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1565c0;stop-opacity:1" />
</linearGradient>
<linearGradient id="claw" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:1" />
<stop offset="100%" style="stop-color:#e3f2fd;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="60" cy="60" r="56" fill="url(#bg)" />
<!-- Pico Claw icon -->
<g transform="translate(60,55)">
<!-- Body -->
<ellipse cx="0" cy="5" rx="22" ry="18" fill="url(#claw)" opacity="0.95"/>
<!-- Eyes -->
<circle cx="-8" cy="-2" r="4" fill="#1e88e5"/>
<circle cx="8" cy="-2" r="4" fill="#1e88e5"/>
<circle cx="-7" cy="-3" r="1.5" fill="white"/>
<circle cx="9" cy="-3" r="1.5" fill="white"/>
<!-- Antenna -->
<line x1="0" y1="-13" x2="0" y2="-28" stroke="url(#claw)" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="0" cy="-30" r="4" fill="#ff7043"/>
<!-- Claws left -->
<path d="M-18,10 Q-28,0 -24,-8" stroke="url(#claw)" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M-16,14 Q-30,8 -28,-2" stroke="url(#claw)" stroke-width="3" fill="none" stroke-linecap="round"/>
<!-- Claws right -->
<path d="M18,10 Q28,0 24,-8" stroke="url(#claw)" stroke-width="3" fill="none" stroke-linecap="round"/>
<path d="M16,14 Q30,8 28,-2" stroke="url(#claw)" stroke-width="3" fill="none" stroke-linecap="round"/>
</g>
<!-- OpenWrt text hint -->
<text x="60" y="98" text-anchor="middle" font-family="Arial,sans-serif" font-size="9" font-weight="bold" fill="white" opacity="0.8">OpenWrt</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View file

@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""
PicoClaw + LuCI One-Click Installer for OpenWrt
Supports OpenWrt 24.10 / 25.xx (ARM64 / AMD64 / ARMv7)
Usage:
pip install paramiko
python install.py
"""
import paramiko
import sys
import os
import time
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
# NOTE: The LuCI controller and view template are deployed from the actual files
# in this repository (luci/controller/picoclaw.lua and luci/view/picoclaw/main.htm),
# NOT from inline string literals. This ensures the deployed code always matches
# the latest version in the repo.
#
# LuCI controller: luci/controller/picoclaw.lua
# LuCI view template: luci/view/picoclaw/main.htm
def print_step(msg):
print(f"\n{'='*55}")
print(f" {msg}")
print(f"{'='*55}")
def run_cmd(client, cmd, timeout=120):
stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout)
try:
out = stdout.read().decode('utf-8', errors='replace').strip()
except:
out = "(timeout)"
try:
err = stderr.read().decode('utf-8', errors='replace').strip()
except:
err = ""
if out:
print(f" {out}")
if err and 'warning' not in err.lower():
print(f" [ERR] {err}")
return out, err
def main():
print("""
PicoClaw + LuCI One-Click Installer
OpenWrt 24.10 / 25.xx (ARM64/AMD64/ARMv7)
https://github.com/sipeed/picoclaw
""")
host = input("Router IP [default: 192.168.1.1]: ").strip() or "192.168.1.1"
port = int(input("SSH Port [default: 22]: ").strip() or "22")
user = input("SSH User [default: root]: ").strip() or "root"
pwd = input("SSH Password: ").strip()
if not pwd:
print("Error: password required")
sys.exit(1)
print(f"\nConnecting to {user}@{host}:{port} ...")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
client.connect(host, port=port, username=user, password=pwd, timeout=15)
except Exception as e:
print(f"Connection failed: {e}")
sys.exit(1)
print("Connected!")
def run(cmd, timeout=120):
return run_cmd(client, cmd, timeout)
# Step 1: Detect system
print_step("Step 1/6: Detect System")
arch_out, _ = run("uname -m")
arch = "linux_arm64"
if "x86" in arch_out:
arch = "linux_amd64"
elif "armv7" in arch_out:
arch = "linux_armv7"
print(f" Architecture: {arch_out} -> {arch}")
release_out, _ = run("cat /etc/openwrt_release 2>/dev/null | grep DISTRIB_DESCRIPTION")
print(f" System: {release_out}")
has_luci, _ = run("test -d /usr/lib/lua/luci && echo 'yes' || echo 'no'")
# Step 2: Install PicoClaw
print_step("Step 2/6: Install PicoClaw")
existing_ver, _ = run("picoclaw version 2>&1 || echo 'not installed'")
if 'not installed' not in existing_ver and existing_ver:
print(f" Already installed: {existing_ver}")
reinstall = input(" Reinstall? (y/N): ").strip().lower()
if reinstall != 'y':
print(" Skipped")
else:
run(f"curl -L -o /tmp/picoclaw_new 'https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_{arch}' --max-time 120")
run("chmod +x /tmp/picoclaw_new; cp /usr/bin/picoclaw /usr/bin/picoclaw.bak 2>/dev/null; mv /tmp/picoclaw_new /usr/bin/picoclaw")
run("picoclaw version 2>&1")
else:
print(" Downloading PicoClaw...")
run(f"curl -L -o /tmp/picoclaw 'https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_{arch}' --max-time 120")
run("chmod +x /tmp/picoclaw; mkdir -p /usr/bin; cp /tmp/picoclaw /usr/bin/picoclaw; rm -f /tmp/picoclaw")
run("picoclaw version 2>&1")
# Step 3: init.d service
print_step("Step 3/6: Create init.d Service")
sftp = client.open_sftp()
initd_script = open(os.path.join(os.path.dirname(__file__), 'scripts', 'picoclaw.init'), 'r').read()
with sftp.open('/etc/init.d/picoclaw', 'w') as f:
f.write(initd_script)
print(" Written /etc/init.d/picoclaw")
run("chmod +x /etc/init.d/picoclaw; /etc/init.d/picoclaw enable 2>&1")
print(" Auto-start enabled")
# Step 4: Deploy LuCI
if has_luci.strip() == 'yes':
print_step("Step 4/6: Deploy LuCI Interface")
controller_path = os.path.join(os.path.dirname(__file__), 'luci', 'controller', 'picoclaw.lua')
template_path = os.path.join(os.path.dirname(__file__), 'luci', 'view', 'picoclaw', 'main.htm')
with sftp.open('/usr/lib/lua/luci/controller/picoclaw.lua', 'w') as f:
f.write(open(controller_path, 'r').read())
print(" Controller deployed")
run("mkdir -p /usr/lib/lua/luci/view/picoclaw")
with sftp.open('/usr/lib/lua/luci/view/picoclaw/main.htm', 'w') as f:
f.write(open(template_path, 'r').read())
print(" Template deployed")
run("rm -rf /tmp/luci-* 2>/dev/null")
print(" LuCI cache cleared")
luci_url = f"http://{host}/cgi-bin/luci/admin/services/picoclaw"
else:
print_step("Step 4/6: Skipped (LuCI not detected)")
luci_url = ""
sftp.close()
# Step 5: Init config
print_step("Step 5/6: Initialize Config")
run("mkdir -p /root/.picoclaw")
has_config, _ = run("test -f /root/.picoclaw/config.json && echo 'yes' || echo 'no'")
if has_config.strip() != 'yes':
print(" Running picoclaw gateway to generate default config...")
run("picoclaw gateway >/dev/null 2>&1 &")
time.sleep(3)
run("pkill -f 'picoclaw gateway' 2>/dev/null")
time.sleep(1)
print(" Default config generated")
else:
print(" Config exists, skipped")
# Step 6: Start service
print_step("Step 6/6: Start PicoClaw")
run("pkill -f 'picoclaw gateway' 2>/dev/null")
time.sleep(1)
run("/etc/init.d/picoclaw start 2>&1")
time.sleep(3)
ps_out, _ = run("ps | grep 'picoclaw gateway' | grep -v grep")
print(f"\n{'='*55}")
print(f" Installation Complete!")
print(f"{'='*55}")
if ps_out:
print(f" [OK] PicoClaw is running")
else:
print(f" [!] PicoClaw not detected, check: picoclaw gateway")
if luci_url:
print(f"\n LuCI Manager: {luci_url}")
print(f" API: http://{host}:18790")
print(f" Config: /root/.picoclaw/config.json")
client.close()
print("\nDone!")
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print("\nCancelled")
sys.exit(0)
except Exception as e:
print(f"\nError: {e}")
sys.exit(1)

View file

@ -0,0 +1,333 @@
module("luci.controller.picoclaw", package.seeall)
function index()
entry({"admin", "services", "picoclaw"}, call("action_main"), "PicoClaw", 60)
entry({"admin", "services", "picoclaw", "action"}, call("action_do"), nil)
end
function get_status()
local pid = ""
local running = false
local memory_kb = 0
local port_active = false
local f = io.popen("ps | grep 'picoclaw gateway' | grep -v grep | head -1 2>/dev/null")
if f then
local line = f:read("*l") or ""
f:close()
local p = line:match("^%s*(%d+)")
if p and p ~= "" then
pid = p
running = true
end
end
if running and pid ~= "" then
local mf = io.open("/proc/" .. pid .. "/status", "r")
if mf then
local c = mf:read("*a")
mf:close()
local vm = c:match("VmRSS:%s*(%d+)")
if vm then memory_kb = tonumber(vm) or 0 end
end
end
local nf = io.open("/proc/net/tcp6", "r")
if not nf then nf = io.open("/proc/net/tcp", "r") end
if nf then
local c = nf:read("*a")
nf:close()
if c:find(":4966") then port_active = true end
end
return {running=running, pid=pid, memory_kb=memory_kb, port_active=port_active}
end
function get_config()
local f = io.open("/root/.picoclaw/config.json", "r")
if not f then return nil, "Config file not found" end
local c = f:read("*a")
f:close()
return c, nil
end
function get_version_info()
local cur_ver = "N/A"
local build_time = ""
local git_commit = ""
local f = io.open("/root/.picoclaw/config.json", "r")
if f then
local c = f:read("*a")
f:close()
local v = c:match('"version"%s*:%s*"([^"]+)"')
if v then cur_ver = v end
local bt = c:match('"build_time"%s*:%s*"([^"]+)"')
if bt then build_time = bt end
local gc = c:match('"git_commit"%s*:%s*"([^"]+)"')
if gc then git_commit = gc end
end
return cur_ver, build_time, git_commit
end
function check_latest_version()
local latest_ver = ""
local latest_url = ""
local err_msg = ""
local cache_file = "/tmp/picoclaw_latest_ver"
local cf = io.open(cache_file, "r")
if cf then
local cached = cf:read("*a")
cf:close()
local v = cached:match("^([%d.]+)")
local u = cached:match("\n(.+)$")
local ts = 0
local tf = io.open(cache_file .. ".ts", "r")
if tf then
ts = tonumber(tf:read("*l")) or 0
tf:close()
end
if v and ts and (os.time() - ts < 3600) then
return v, u or "", ""
end
end
local f = io.popen("curl -sL --max-time 5 'https://api.github.com/repos/sipeed/picoclaw/releases/latest' 2>/dev/null")
if f then
local body = f:read("*a")
f:close()
local v = body:match('"tag_name"%s*:%s*"v?([^"]+)"')
if v then latest_ver = v end
local u = body:match('"browser_download_url"%s*:%s*"(.-linux_arm64)"')
if u then latest_url = u end
else
err_msg = "curl failed"
end
if latest_ver ~= "" then
local cf2 = io.open(cache_file, "w")
if cf2 then
cf2:write(latest_ver .. "\n" .. latest_url)
cf2:close()
end
local tf = io.open(cache_file .. ".ts", "w")
if tf then
tf:write(tostring(os.time()))
tf:close()
end
end
if latest_ver == "" then err_msg = "checking" end
return latest_ver, latest_url, err_msg
end
function get_logs()
local f = io.popen("logread 2>/dev/null | grep -i picoclaw | tail -50")
if not f then return "" end
local l = f:read("*a") or ""
f:close()
if l == "" then
local f2 = io.popen("logread 2>/dev/null | tail -30")
if f2 then
l = f2:read("*a") or ""
f2:close()
end
end
return l
end
function html_escape(s)
if not s then return "" end
s = tostring(s)
s = s:gsub("&", "&amp;")
s = s:gsub("<", "&lt;")
s = s:gsub(">", "&gt;")
s = s:gsub('"', "&quot;")
return s
end
function do_update()
local arch = "linux_arm64"
local f = io.popen("uname -m 2>/dev/null")
if f then
local m = f:read("*l") or ""
f:close()
if m:find("x86") then arch = "linux_amd64" end
end
local dl_url = "https://github.com/sipeed/picoclaw/releases/latest/download/picoclaw_" .. arch
os.execute("pkill -f 'picoclaw gateway' 2>/dev/null")
os.execute("sleep 1")
os.execute("curl -L -o /tmp/picoclaw_new '" .. dl_url .. "' --max-time 120 2>&1")
os.execute("chmod +x /tmp/picoclaw_new")
os.execute("cp /usr/bin/picoclaw /usr/bin/picoclaw.bak 2>/dev/null")
os.execute("mv /tmp/picoclaw_new /usr/bin/picoclaw")
os.execute("picoclaw gateway >/dev/null 2>&1 &")
os.execute("sleep 3")
end
function action_do()
local http = require("luci.http")
local action = http.formvalue("action") or ""
local msg = ""
local ok = true
if action == "start" then
os.execute("picoclaw gateway >/dev/null 2>&1 &")
os.execute("sleep 2")
msg = "服务正在启动..."
elseif action == "stop" then
os.execute("pkill -f 'picoclaw gateway' 2>/dev/null")
os.execute("sleep 1")
msg = "服务已停止。"
elseif action == "restart" then
os.execute("pkill -f 'picoclaw gateway' 2>/dev/null")
os.execute("sleep 1")
os.execute("picoclaw gateway >/dev/null 2>&1 &")
os.execute("sleep 2")
msg = "服务已重启。"
elseif action == "autostart_on" then
os.execute("/etc/init.d/picoclaw enable 2>/dev/null; ln -sf /etc/init.d/picoclaw /etc/rc.d/S99picoclaw 2>/dev/null")
msg = "已启用开机自动启动。"
elseif action == "autostart_off" then
os.execute("/etc/init.d/picoclaw disable 2>/dev/null; rm -f /etc/rc.d/S99picoclaw 2>/dev/null")
msg = "已关闭开机自动启动。"
elseif action == "save_config" then
local config = http.formvalue("config") or ""
if config ~= "" then
local f = io.open("/root/.picoclaw/config.json", "w")
if f then
f:write(config)
f:close()
os.execute("pkill -f 'picoclaw gateway' 2>/dev/null; sleep 1; picoclaw gateway >/dev/null 2>&1 &")
msg = "配置已保存,服务已重启!"
else
msg = "错误:无法写入配置文件"
ok = false
end
else
msg = "错误:配置内容为空"
ok = false
end
elseif action == "save_form_config" then
local config = http.formvalue("config") or ""
if config ~= "" then
local f = io.open("/root/.picoclaw/config.json", "w")
if f then
f:write(config)
f:close()
os.execute("pkill -f 'picoclaw gateway' 2>/dev/null; sleep 1; picoclaw gateway >/dev/null 2>&1 &")
msg = "配置已保存,服务已重启!"
else
msg = "错误:无法写入配置文件"
ok = false
end
else
msg = "错误:配置内容为空"
ok = false
end
elseif action == "update" then
do_update()
msg = "更新完成,服务已重启!"
end
local dispatcher = require("luci.dispatcher")
local url = dispatcher.build_url("admin", "services", "picoclaw")
if msg ~= "" then
url = url .. "?msg=" .. luci.http.urlencode(msg) .. "&ok=" .. (ok and "1" or "0")
end
luci.http.redirect(url)
end
function action_main()
local http = require("luci.http")
local status = get_status()
local config_content, config_err = get_config()
local logs = get_logs()
local cur_ver, build_time, git_commit = get_version_info()
local latest_ver, latest_url, check_err = check_latest_version()
local has_update = false
if latest_ver ~= "" and cur_ver ~= "N/A" then
local function ver_parts(v)
local t = {}
for n in v:gmatch("%d+") do
t[#t + 1] = tonumber(n)
end
return t
end
local cv = ver_parts(cur_ver)
local lv = ver_parts(latest_ver)
for i = 1, math.max(#cv, #lv) do
local a = cv[i] or 0
local b = lv[i] or 0
if b > a then
has_update = true
break
end
if a > b then
break
end
end
end
local memory_mb = "0.0"
if status.memory_kb and tonumber(status.memory_kb) then
memory_mb = string.format("%.1f", tonumber(status.memory_kb) / 1024)
end
local pid_str = "-"
if status.pid and status.pid ~= "" then
pid_str = tostring(status.pid)
end
local weixin_status = "none"
local weixin_configured = false
if config_content then
local ws = config_content:find('"weixin"')
if ws then
local wblock = config_content:sub(ws, ws + 2000)
local wenabled = false
local wep = wblock:find('"enabled"')
if wep then
local wrest = wblock:sub(wep + 9)
wrest = wrest:match("^[%s]*:[%s]*(.*)")
if wrest and wrest:sub(1, 4) == "true" then
local wnc = wrest:sub(5, 5)
if wnc == "" or wnc == "," or wnc == "}" or wnc:match("%s") then
wenabled = true
end
end
end
local burl = wblock:match('"base_url"%s*:%s*"([^"]+)"')
weixin_configured = (burl ~= nil and burl ~= "")
if wenabled then
weixin_status = "connected"
elseif weixin_configured then
weixin_status = "configured"
end
end
end
local flash_msg = http.formvalue("msg") or ""
local flash_ok = http.formvalue("ok") or "1"
local autostart = false
local asf = io.open("/etc/rc.d/S99picoclaw", "r")
if asf then asf:close() autostart = true end
luci.template.render("picoclaw/main", {
running = status.running,
pid = pid_str,
memory_mb = memory_mb,
port_active = status.port_active or false,
cur_ver = html_escape(cur_ver),
latest_ver = html_escape(latest_ver),
build_time = html_escape(build_time),
git_commit = html_escape(git_commit),
latest_url = html_escape(latest_url),
has_update = has_update,
check_err = check_err,
config_content = html_escape(config_content or ""),
config_raw = (config_content or ""),
weixin_status = weixin_status,
weixin_configured = weixin_configured,
channels_html = "",
logs = html_escape(logs),
flash_msg = html_escape(flash_msg),
flash_ok = flash_ok,
action_url = luci.dispatcher.build_url("admin", "services", "picoclaw", "action"),
autostart = autostart
})
end

View file

@ -0,0 +1,810 @@
<%+header%>
<style>
.pc{max-width:960px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
.pc-card{background:#fff;border:1px solid #e0e0e0;border-radius:10px;padding:20px 24px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
.pc-status{display:flex;align-items:center;gap:14px;padding:16px;border-radius:8px;flex-wrap:wrap}
.pc-status.run{background:#e8f5e9}.pc-status.stop{background:#ffebee}
.dot{width:14px;height:14px;border-radius:50%}
.dot.on{background:#4caf50;box-shadow:0 0 8px #4caf50}.dot.off{background:#f44336}
.pc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-top:14px}
.pc-metric{text-align:center;padding:12px;background:#f8f9fa;border-radius:6px}
.pc-metric .val{font-size:22px;font-weight:700;color:#333}
.pc-metric .lbl{font-size:12px;color:#888;margin-top:4px}
.pc-btn{display:inline-block;padding:7px 18px;border:none;border-radius:6px;cursor:pointer;font-size:13px;font-weight:600;margin:2px;transition:all .2s;text-decoration:none;color:#fff}
.pc-btn:active{transform:scale(.97)}.pc-btn:hover{box-shadow:0 2px 8px rgba(0,0,0,.15)}
.pc-btn.green{background:#43a047}.pc-btn.orange{background:#ef6c00}
.pc-btn.blue{background:#1e88e5}.pc-btn.purple{background:#8e24aa}
.pc-btn.gray{background:#616161}.pc-btn:visited{color:#fff}
.pc-tabs{display:flex;border-bottom:2px solid #e0e0e0;margin-bottom:0}
.pc-tab{padding:10px 20px;cursor:pointer;font-size:14px;color:#888;border:none;background:none;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .2s}
.pc-tab.act{color:#1976d2;border-bottom-color:#1976d2;font-weight:600}
.pc-tab:hover{color:#1976d2}
.pc-panel{display:none;padding-top:16px}.pc-panel.act{display:block}
.ch-row{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid #f0f0f0}
.ch-row:last-child{border:none}
.ch-name{font-size:14px;font-weight:500}
.badge{display:inline-block;padding:3px 12px;border-radius:12px;font-size:12px;font-weight:600}
.badge-on{background:#c8e6c9;color:#2e7d32}.badge-off{background:#ffcdd2;color:#c62828}
.cfg-section{margin-bottom:20px}.cfg-section:last-child{margin-bottom:0}
.cfg-section-head{display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;padding:12px 16px;background:#f5f7fa;border-radius:8px;margin-bottom:2px;transition:background .2s}
.cfg-section-head:hover{background:#eef1f6}
.cfg-section-head .icon{font-size:18px;width:28px;text-align:center;flex-shrink:0}
.cfg-section-head h4{margin:0;font-size:15px;font-weight:600;color:#333;flex:1}
.cfg-section-head .arrow{font-size:12px;color:#999;transition:transform .2s}
.cfg-section.closed .arrow{transform:rotate(-90deg)}
.cfg-section-body{padding:14px 16px;border:1px solid #eee;border-top:none;border-radius:0 0 8px 8px;background:#fafbfc}
.cfg-section.closed .cfg-section-body{display:none}
.cfg-row{display:flex;align-items:flex-start;gap:12px;padding:10px 0;border-bottom:1px solid #f0f0f0}
.cfg-row:last-child{border-bottom:none}
.cfg-label{flex:0 0 160px;font-size:13px;color:#555;padding-top:6px;line-height:1.4}
.cfg-label small{display:block;font-size:11px;color:#999;margin-top:2px}
.cfg-field{flex:1;min-width:0}
.cfg-input{width:100%;padding:8px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;box-sizing:border-box;transition:border-color .2s,box-shadow .2s;background:#fff}
.cfg-input:focus{outline:none;border-color:#1976d2;box-shadow:0 0 0 3px rgba(25,118,210,.1)}
.cfg-input-sm{width:200px}.cfg-input-xs{width:80px}
.toggle-wrap{display:flex;align-items:center;gap:10px;padding-top:4px}
.toggle{position:relative;width:44px;height:24px;flex-shrink:0}
.toggle input{opacity:0;width:0;height:0}
.toggle .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#ccc;border-radius:24px;transition:all .25s}
.toggle .slider:before{content:"";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:all .25s}
.toggle input:checked+.slider{background:#1976d2}
.toggle input:checked+.slider:before{transform:translateX(20px)}
.toggle-label{font-size:13px;color:#666;transition:color .2s}
.toggle-label.on{color:#2e7d32}.toggle-label.off{color:#999}
.cfg-mode-bar{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap}
.mode-label{font-size:13px;color:#666;font-weight:600}
.cfg-mode-btns{display:flex;gap:6px}
.cfg-mode-btn{padding:6px 16px;border:2px solid #e0e0e0;border-radius:6px;background:#fff;cursor:pointer;font-size:13px;color:#666;transition:all .2s}
.cfg-mode-btn.act{border-color:#1976d2;color:#1976d2;background:#e3f2fd;font-weight:600}
.cfg-mode-btn:hover{border-color:#90caf9}
.pc-actions{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.pc-editor{width:100%;min-height:400px;font-family:'Courier New',monospace;font-size:13px;padding:12px;border:1px solid #ddd;border-radius:8px;box-sizing:border-box;resize:vertical;background:#fafbfc}
.pc-log{background:#1e1e1e;color:#d4d4d4;padding:14px;border-radius:8px;font-family:'Courier New',monospace;font-size:12px;max-height:400px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;line-height:1.6}
.update-banner{display:flex;align-items:center;gap:14px;padding:14px;border-radius:8px;margin-bottom:14px}
.update-banner.available{background:#fff3e0}.update-banner.latest{background:#e8f5e9}.update-banner.error{background:#f5f5f5}
.update-icon{font-size:28px;flex-shrink:0}
.update-text{flex:1}.update-text strong{display:block;font-size:14px}.update-text .detail{font-size:12px;color:#666;margin-top:2px}
.ver-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f0f0f0;font-size:13px}
.ver-row:last-child{border:none}
.ver-row .label{color:#888}.ver-row .value{color:#333;font-weight:500}
.flash-ok{padding:12px 16px;border-radius:8px;margin-bottom:16px;font-size:13px}
.flash-ok.flash-ok{background:#e8f5e9;color:#2e7d32;border:1px solid #c8e6c9}
.flash-ok.flash-err{background:#ffebee;color:#c62828;border:1px solid #ffcdd2}
.lang-bar{display:flex;gap:4px}
.lang-btn{padding:4px 10px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-size:12px;color:#888;transition:all .15s}
.lang-btn.act{background:#1976d2;color:#fff;border-color:#1976d2}
.lang-btn:hover{border-color:#90caf9}
</style>
<%
local flash_msg = flash_msg or ""
local flash_ok = flash_ok or "1"
if flash_msg ~= "" then
%>
<div class="pc-card" style="padding:0">
<div class="<% if flash_ok == "1" then %>flash-ok<% else %>flash-err<% end %>">
<% if flash_ok == "1" then %>&#10003;<% else %>&#9888;<% end %> <%=flash_msg%>
</div>
</div>
<% end %>
<div class="pc">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h2 style="margin:0;font-size:20px;font-weight:700;color:#333">PicoClaw AI</h2>
<div class="lang-bar">
<button class="lang-btn act" onclick="setLang('zh')">中文</button>
<button class="lang-btn" onclick="setLang('en')">EN</button>
<button class="lang-btn" onclick="setLang('ja')">日本語</button>
<button class="lang-btn" onclick="setLang('pt')">PT</button>
<button class="lang-btn" onclick="setLang('de')">DE</button>
</div>
</div>
<!-- Version Card -->
<div class="pc-card">
<h3 data-i18n="version_info">版本信息</h3>
<% if has_update then %>
<div class="update-banner available">
<div class="update-icon">&#128679;</div>
<div class="update-text">
<strong data-i18n="update_available">发现新版本</strong>
<div class="detail">v<%=cur_ver%> → v<%=latest_ver%></div>
</div>
<form method="post" action="<%=action_url%>" style="display:inline;flex-shrink:0">
<input type="hidden" name="action" value="update">
<button type="submit" class="pc-btn purple" onclick="return confirm(I('confirm_update'))" data-i18n="update_now">立即更新</button>
</form>
</div>
<% elseif latest_ver ~= "" and not has_update then %>
<div class="update-banner latest">
<div class="update-icon">&#9989;</div>
<div class="update-text">
<strong data-i18n="latest_version">已是最新版本</strong>
<div class="detail">v<%=cur_ver%></div>
</div>
</div>
<% elseif check_err ~= "" then %>
<div class="update-banner error">
<div class="update-icon">&#128268;</div>
<div class="update-text">
<strong data-i18n="check_failed">版本检测中...</strong>
<div class="detail">v<%=cur_ver%></div>
</div>
</div>
<% end %>
<div class="ver-row"><span class="label" data-i18n="current_version">当前版本</span><span class="value">v<%=cur_ver%></span></div>
<% if git_commit ~= "" then %>
<div class="ver-row"><span class="label">Git Commit</span><span class="value" style="font-family:monospace;font-size:13px"><%=git_commit%></span></div>
<% end %>
<% if build_time ~= "" then %>
<div class="ver-row"><span class="label" data-i18n="build_time">构建时间</span><span class="value"><%=build_time%></span></div>
<% end %>
<% if latest_ver ~= "" then %>
<div class="ver-row"><span class="label" data-i18n="latest_ver_label">最新版本</span><span class="value">v<%=latest_ver%></span></div>
<% end %>
</div>
<!-- Status Card -->
<div class="pc-card">
<h3 data-i18n="service_status">服务状态</h3>
<div class="pc-status <% if running then %>run<% else %>stop<% end %>">
<div class="dot <% if running then %>on<% else %>off<% end %>"></div>
<div>
<strong style="font-size:16px"><% if running then %><span data-i18n="running">运行中</span><% else %><span data-i18n="stopped">已停止</span><% end %></strong>
<div style="font-size:12px;color:#666;margin-top:2px">
<% if running then %>PID: <%=pid%> | Port 18790: <% if port_active then %><span data-i18n="normal">正常</span><% else %><span data-i18n="abnormal">异常</span><% end %><% else %><span data-i18n="service_not_running">服务未运行</span><% end %>
</div>
</div>
<div style="margin-left:auto;display:flex;gap:6px;flex-wrap:wrap;align-items:center">
<form method="post" action="<%=action_url%>" style="display:inline"><input type="hidden" name="action" value="start"><button type="submit" class="pc-btn green" data-i18n="start">启动</button></form>
<form method="post" action="<%=action_url%>" style="display:inline"><input type="hidden" name="action" value="stop"><button type="submit" class="pc-btn orange" data-i18n="stop">停止</button></form>
<form method="post" action="<%=action_url%>" style="display:inline"><input type="hidden" name="action" value="restart"><button type="submit" class="pc-btn blue" data-i18n="restart">重启</button></form>
<div style="width:1px;height:24px;background:#ddd;margin:0 4px"></div>
<% if autostart then %>
<form method="post" action="<%=action_url%>" style="display:inline"><input type="hidden" name="action" value="autostart_off"><button type="submit" class="pc-btn" style="background:#e8f5e9;color:#2e7d32;border-color:#c8e6c9" data-i18n="autostart_on">&#9889; 开机自启: 开</button></form>
<% else %>
<form method="post" action="<%=action_url%>" style="display:inline"><input type="hidden" name="action" value="autostart_on"><button type="submit" class="pc-btn gray" data-i18n="autostart_off">&#9889; 开机自启: 关</button></form>
<% end %>
</div>
</div>
<div class="pc-grid">
<div class="pc-metric"><div class="val"><%=memory_mb%></div><div class="lbl" data-i18n="memory_mb">内存 (MB)</div></div>
<div class="pc-metric"><div class="val"><%=pid%></div><div class="lbl" data-i18n="process_id">进程 ID</div></div>
<div class="pc-metric"><div class="val"><% if port_active then %><span data-i18n="normal">正常</span><% else %><span data-i18n="abnormal">异常</span><% end %></div><div class="lbl" data-i18n="port_18790">端口 18790</div></div>
</div>
</div>
<!-- Tabs -->
<div class="pc-card" style="padding:0;overflow:hidden">
<div class="pc-tabs" style="padding:0 24px">
<button class="pc-tab act" onclick="switchTab('channels',this)" data-i18n="tab_channels">通道列表</button>
<button class="pc-tab" onclick="switchTab('config',this)" data-i18n="tab_config">配置编辑</button>
<button class="pc-tab" onclick="switchTab('logs',this)" data-i18n="tab_logs">系统日志</button>
</div>
<!-- Channels Panel -->
<div class="pc-panel act" id="panel-channels" style="padding:16px 24px 24px">
<div id="channels-list"></div>
</div>
<!-- Config Panel -->
<div class="pc-panel" id="panel-config" style="padding:16px 24px 24px">
<div class="cfg-mode-bar">
<span class="mode-label" data-i18n="select_mode">选择编辑模式:</span>
<div class="cfg-mode-btns">
<button class="cfg-mode-btn act" onclick="switchCfgMode('form',this)">&#127968; <span data-i18n="mode_form">表单模式</span></button>
<button class="cfg-mode-btn" onclick="switchCfgMode('json',this)">{ } <span data-i18n="mode_json">JSON 模式</span></button>
</div>
</div>
<!-- Form Mode -->
<div id="cfg-form-mode">
<form id="config-form" method="post" action="<%=action_url%>">
<input type="hidden" name="action" value="save_form_config">
<input type="hidden" name="config" id="form-config-json" value="">
<!-- AI Model Section -->
<div class="cfg-section">
<div class="cfg-section-head" onclick="toggleSection(this)">
<span class="icon">&#129302;</span>
<h4 data-i18n="sec_ai_model">AI 模型配置</h4>
<span class="arrow">&#9660;</span>
</div>
<div class="cfg-section-body">
<div class="cfg-row">
<div class="cfg-label" data-i18n="lbl_provider">AI 提供商<small>provider</small></div>
<div class="cfg-field"><select class="cfg-input cfg-input-sm" data-path="agents.defaults.provider">
<option value="zhipu">Zhipu / 智谱</option>
<option value="openai">OpenAI</option>
<option value="chatgpt">ChatGPT / OpenAI GPT</option>
<option value="claude">Claude / Anthropic</option>
<option value="deepseek">DeepSeek</option>
<option value="anthropic">Anthropic</option>
<option value="ollama">Ollama</option>
<option value="azure">Azure OpenAI</option>
</select></div>
</div>
<div class="cfg-row">
<div class="cfg-label" data-i18n="lbl_model">模型名称<small>model_name</small></div>
<div class="cfg-field"><input type="text" class="cfg-input cfg-input-sm" data-path="agents.defaults.model_name" placeholder="glm-4-air"></div>
</div>
<div class="cfg-row">
<div class="cfg-label" data-i18n="lbl_max_tokens">Token 上限<small>max_tokens</small></div>
<div class="cfg-field"><input type="number" class="cfg-input cfg-input-xs" data-path="agents.defaults.max_tokens" min="100" max="200000"></div>
</div>
<div class="cfg-row">
<div class="cfg-label" data-i18n="lbl_tool_iters">迭代次数<small>max_tool_iterations</small></div>
<div class="cfg-field"><input type="number" class="cfg-input cfg-input-xs" data-path="agents.defaults.max_tool_iterations" min="1" max="50"></div>
</div>
<div class="cfg-row">
<div class="cfg-label" data-i18n="lbl_steering">并发模式<small>steering_mode</small></div>
<div class="cfg-field"><select class="cfg-input cfg-input-sm" data-path="steering_mode">
<option value="one-at-a-time">one-at-a-time</option>
<option value="parallel">parallel</option>
</select></div>
</div>
</div>
</div>
<!-- Channels Section -->
<div class="cfg-section">
<div class="cfg-section-head" onclick="toggleSection(this)">
<span class="icon">&#128225;</span>
<h4 data-i18n="sec_channels">通道管理</h4>
<span class="arrow">&#9660;</span>
</div>
<div class="cfg-section-body" id="channels-form-body"></div>
</div>
<!-- Providers Section -->
<div class="cfg-section">
<div class="cfg-section-head" onclick="toggleSection(this)">
<span class="icon">&#128273;</span>
<h4 data-i18n="sec_providers">API 密钥与提供商</h4>
<span class="arrow">&#9660;</span>
</div>
<div class="cfg-section-body" id="providers-form-body"></div>
</div>
<!-- System Section -->
<div class="cfg-section">
<div class="cfg-section-head" onclick="toggleSection(this)">
<span class="icon">&#9881;</span>
<h4 data-i18n="sec_system">系统设置</h4>
<span class="arrow">&#9660;</span>
</div>
<div class="cfg-section-body" id="system-form-body"></div>
</div>
<!-- Tools Section -->
<div class="cfg-section closed">
<div class="cfg-section-head" onclick="toggleSection(this)">
<span class="icon">&#128295;</span>
<h4 data-i18n="sec_tools">工具与功能</h4>
<span class="arrow">&#9660;</span>
</div>
<div class="cfg-section-body" id="tools-form-body"></div>
</div>
<div class="pc-actions">
<button type="button" class="pc-btn green" onclick="saveFormConfig()" data-i18n="save_restart">&#128190; 保存并重启</button>
<button type="button" class="pc-btn gray" onclick="loadFormFromJSON()" data-i18n="reset_form">&#128260; 重置</button>
</div>
</form>
</div>
<!-- JSON Mode -->
<div id="cfg-json-mode" style="display:none">
<form method="post" action="<%=action_url%>">
<input type="hidden" name="action" value="save_config">
<textarea name="config" class="pc-editor" id="json-editor"><%=config_content%></textarea>
<div class="pc-actions">
<button type="submit" class="pc-btn green" onclick="return confirm(I('confirm_save'))">&#128190; <span data-i18n="save_restart">保存并重启</span></button>
<button type="button" class="pc-btn blue" onclick="formatJSON()">&#128221; <span data-i18n="format_json">格式化</span></button>
<button type="button" class="pc-btn gray" onclick="loadJSONFromForm()">&#128260; <span data-i18n="sync_from_form">从表单同步</span></button>
</div>
</form>
</div>
</div>
<!-- Logs Panel -->
<div class="pc-panel" id="panel-logs" style="padding:16px 24px 24px">
<div class="pc-log"><% if logs and logs ~= "" then %><%=logs%><% else %><span data-i18n="no_logs">暂无 picoclaw 日志</span><% end %></div>
<div style="margin-top:8px"><a href="<%=action_url%>" class="pc-btn blue" data-i18n="refresh_logs">刷新日志</a></div>
</div>
</div>
</div>
<script>
var _origConfig = <%=config_raw or "{}"%>;
var _curLang = localStorage.getItem('pc_lang') || 'zh';
// ========== i18n ==========
var T = {
zh: {
version_info:"版本信息", current_version:"当前版本", build_time:"构建时间", latest_ver_label:"最新版本",
latest_version:"已是最新版本", update_available:"发现新版本", update_now:"立即更新",
confirm_update:"确定要更新 PicoClaw 吗?更新过程中服务会短暂中断。",
check_failed:"版本检测中...", service_status:"服务状态", running:"运行中", stopped:"已停止",
service_not_running:"服务未运行", start:"启动", stop:"停止", restart:"重启", normal:"正常", abnormal:"异常",
autostart_on:"开机自启: 开", autostart_off:"开机自启: 关",
memory_mb:"内存 (MB)", process_id:"进程 ID", port_18790:"端口 18790",
tab_channels:"通道列表", tab_config:"配置编辑", tab_logs:"系统日志",
select_mode:"选择编辑模式:", mode_form:"表单模式", mode_json:"JSON 模式",
save_restart:"保存并重启", reset_form:"重置", format_json:"格式化", sync_from_form:"从表单同步",
no_logs:"暂无 picoclaw 日志", refresh_logs:"刷新日志",
enabled:"已启用", disabled:"已禁用", no_channels:"暂无配置的通道",
sec_ai_model:"AI 模型配置", lbl_provider:"AI 提供商", lbl_model:"模型名称",
lbl_max_tokens:"Token 上限", lbl_tool_iters:"迭代次数", lbl_steering:"并发模式",
sec_channels:"通道管理", sec_providers:"API 密钥与提供商", sec_system:"系统设置", sec_tools:"工具与功能",
confirm_save:"确定保存配置并重启 PicoClaw", json_error:"JSON 错误:",
ch_feishu:"飞书", ch_telegram:"Telegram", ch_discord:"Discord", ch_wecom:"企业微信",
ch_weixin:"微信个人号", ch_slack:"Slack", ch_qq:"QQ", ch_line:"LINE",
ch_dingtalk:"钉钉", ch_whatsapp:"WhatsApp", ch_maixcam:"MaixCam", ch_wechat_work:"企业微信(WeCom)",
tl_feishu:"飞书机器人", tl_telegram:"Telegram Bot", tl_discord:"Discord Bot",
tl_wecom:"企业微信(WeCom Bot)", tl_weixin:"微信个人号(需扫码授权)",
tl_slack:"Slack Bot", tl_qq:"QQ Bot", tl_line:"LINE Bot",
tl_dingtalk:"钉钉机器人", tl_whatsapp:"WhatsApp", tl_maixcam:"MaixCam 设备",
tl_wechat_work:"企业微信", tl_pico:"Pico 协议", tl_pico_client:"Pico 客户端",
lbl_listen:"监听地址", lbl_heartbeat:"心跳检测", lbl_heartbeat_interval:"心跳间隔 (秒)",
lbl_media_cleanup:"媒体清理", lbl_session_scope:"会话作用域",
tl_heartbeat:"启用心跳", tl_media_cleanup:"自动清理过期媒体文件",
scope_per_peer:"per-channel-peer", scope_per_channel:"per-channel", scope_global:"global",
lbl_exec:"命令执行", lbl_exec_timeout:"执行超时 (秒)", lbl_exec_remote:"远程执行",
lbl_cron:"定时任务", lbl_cron_timeout:"Cron 超时 (分)", lbl_web_fetch:"网页抓取",
lbl_file_rw:"文件读写", lbl_skills:"技能系统", lbl_mcp:"MCP", lbl_subagent:"子代理",
tl_exec:"允许 AI 执行命令", tl_exec_remote:"允许远程执行",
tl_cron:"启用定时任务", tl_web_fetch:"启用网页抓取",
tl_read_file:"允许读取文件", tl_write_file:"允许写入文件",
tl_skills:"启用技能系统", tl_mcp:"启用 MCP", tl_subagent:"启用子代理",
lbl_api_key:"API Key", lbl_base_url:"Base URL", ph_api_key:"粘贴 API Key", ph_optional:"可选",
save_confirm:"确定保存配置并重启 PicoClaw",
weixin_guide_title:"微信个人号接入", weixin_not_connected:"尚未接入微信",
weixin_authorized_not_enabled:"已授权,但通道未启用", weixin_go_enable:"前往启用",
weixin_connected:"微信已连接"
},
en: {
version_info:"Version Info", current_version:"Current Version", build_time:"Build Time", latest_ver_label:"Latest Version",
latest_version:"Up to date", update_available:"New version available", update_now:"Update Now",
confirm_update:"Update PicoClaw? Service will restart briefly.",
check_failed:"Checking for updates...", service_status:"Service Status", running:"Running", stopped:"Stopped",
service_not_running:"Service not running", start:"Start", stop:"Stop", restart:"Restart", normal:"OK", abnormal:"Error",
autostart_on:"Auto-start: ON", autostart_off:"Auto-start: OFF",
memory_mb:"Memory (MB)", process_id:"Process ID", port_18790:"Port 18790",
tab_channels:"Channels", tab_config:"Config Editor", tab_logs:"System Logs",
select_mode:"Mode:", mode_form:"Form", mode_json:"JSON",
save_restart:"Save & Restart", reset_form:"Reset", format_json:"Format", sync_from_form:"Sync from Form",
no_logs:"No picoclaw logs", refresh_logs:"Refresh Logs",
enabled:"Enabled", disabled:"Disabled", no_channels:"No channels configured",
sec_ai_model:"AI Model", lbl_provider:"AI Provider", lbl_model:"Model Name",
lbl_max_tokens:"Max Tokens", lbl_tool_iters:"Max Tool Iterations", lbl_steering:"Steering Mode",
sec_channels:"Channel Management", sec_providers:"API Keys & Providers", sec_system:"System Settings", sec_tools:"Tools & Features",
confirm_save:"Save config and restart PicoClaw?", json_error:"JSON Error: ",
ch_feishu:"Feishu", ch_telegram:"Telegram", ch_discord:"Discord", ch_wecom:"WeCom Bot",
ch_weixin:"WeChat", ch_slack:"Slack", ch_qq:"QQ", ch_line:"LINE",
ch_dingtalk:"DingTalk", ch_whatsapp:"WhatsApp", ch_maixcam:"MaixCam", ch_wechat_work:"WeCom",
tl_feishu:"Feishu Bot", tl_telegram:"Telegram Bot", tl_discord:"Discord Bot",
tl_wecom:"WeCom Bot", tl_weixin:"WeChat Personal (QR auth needed)",
tl_slack:"Slack Bot", tl_qq:"QQ Bot", tl_line:"LINE Bot",
tl_dingtalk:"DingTalk Bot", tl_whatsapp:"WhatsApp", tl_maixcam:"MaixCam Device",
tl_wechat_work:"WeCom Bot", tl_pico:"Pico Protocol", tl_pico_client:"Pico Client",
lbl_listen:"Listen Address", lbl_heartbeat:"Heartbeat", lbl_heartbeat_interval:"Heartbeat Interval (s)",
lbl_media_cleanup:"Media Cleanup", lbl_session_scope:"Session Scope",
tl_heartbeat:"Enable heartbeat", tl_media_cleanup:"Auto cleanup expired media",
scope_per_peer:"per-channel-peer", scope_per_channel:"per-channel", scope_global:"global",
lbl_exec:"Command Exec", lbl_exec_timeout:"Exec Timeout (s)", lbl_exec_remote:"Remote Exec",
lbl_cron:"Cron Jobs", lbl_cron_timeout:"Cron Timeout (min)", lbl_web_fetch:"Web Fetch",
lbl_file_rw:"File Read/Write", lbl_skills:"Skills", lbl_mcp:"MCP", lbl_subagent:"Subagent",
tl_exec:"Allow AI to execute commands", tl_exec_remote:"Allow remote execution",
tl_cron:"Enable cron jobs", tl_web_fetch:"Enable web fetch",
tl_read_file:"Enable file read", tl_write_file:"Enable file write",
tl_skills:"Enable skills", tl_mcp:"Enable MCP", tl_subagent:"Enable subagent",
lbl_api_key:"API Key", lbl_base_url:"Base URL", ph_api_key:"Paste API Key", ph_optional:"Optional",
save_confirm:"Save config and restart PicoClaw?",
weixin_guide_title:"WeChat Setup", weixin_not_connected:"WeChat not connected",
weixin_authorized_not_enabled:"Authorized but channel disabled", weixin_go_enable:"Enable",
weixin_connected:"WeChat Connected"
},
ja: {
version_info:"バージョン情報", current_version:"現在のバージョン", build_time:"ビルド時間", latest_ver_label:"最新バージョン",
latest_version:"最新です", update_available:"新バージョンあり", update_now:"今すぐ更新",
confirm_update:"PicoClawを更新しますかサービスが一時的に再起動します。",
check_failed:"更新確認中...", service_status:"サービス状態", running:"実行中", stopped:"停止",
service_not_running:"サービス未実行", start:"起動", stop:"停止", restart:"再起動", normal:"正常", abnormal:"異常",
autostart_on:"自動起動: ON", autostart_off:"自動起動: OFF",
memory_mb:"メモリ (MB)", process_id:"プロセス ID", port_18790:"ポート 18790",
tab_channels:"チャンネル一覧", tab_config:"設定編集", tab_logs:"システムログ",
select_mode:"編集モード:", mode_form:"フォーム", mode_json:"JSON",
save_restart:"保存&再起動", reset_form:"リセット", format_json:"フォーマット", sync_from_form:"フォームから同期",
no_logs:"picoclawログなし", refresh_logs:"ログ更新",
enabled:"有効", disabled:"無効", no_channels:"設定済みチャンネルなし",
sec_ai_model:"AIモデル設定", lbl_provider:"AIプロバイダー", lbl_model:"モデル名",
lbl_max_tokens:"最大トークン数", lbl_tool_iters:"最大ツール反復", lbl_steering:"並行モード",
sec_channels:"チャンネル管理", sec_providers:"APIキープロバイダー", sec_system:"システム設定", sec_tools:"ツール&機能",
confirm_save:"設定を保存して再起動しますか?", json_error:"JSONエラー: ",
ch_feishu:"Feishu", ch_telegram:"Telegram", ch_discord:"Discord", ch_wecom:"WeCom Bot",
ch_weixin:"WeChat", ch_slack:"Slack", ch_qq:"QQ", ch_line:"LINE",
ch_dingtalk:"DingTalk", ch_whatsapp:"WhatsApp", ch_maixcam:"MaixCam", ch_wechat_work:"WeCom",
tl_feishu:"Feishu Bot", tl_telegram:"Telegram Bot", tl_discord:"Discord Bot",
tl_wecom:"WeCom Bot", tl_weixin:"WeChat個人QR認証必要",
tl_slack:"Slack Bot", tl_qq:"QQ Bot", tl_line:"LINE Bot",
tl_dingtalk:"DingTalk Bot", tl_whatsapp:"WhatsApp", tl_maixcam:"MaixCamデバイス",
tl_wechat_work:"WeCom Bot", lbl_listen:"リスニングアドレス", lbl_heartbeat:"ハートビート", lbl_heartbeat_interval:"ハートビート間隔 (秒)",
lbl_media_cleanup:"メディアクリーニング", lbl_session_scope:"セッションスコープ",
tl_heartbeat:"ハートビート有効", tl_media_cleanup:"期限切れメディアを自動削除",
scope_per_peer:"per-channel-peer", scope_per_channel:"per-channel", scope_global:"global",
lbl_exec:"コマンド実行", lbl_cron:"Cronジョブ", lbl_web_fetch:"Webフェッチ",
lbl_file_rw:"ファイル読書", lbl_skills:"スキル", lbl_mcp:"MCP", lbl_subagent:"サブエージェント",
tl_exec:"AIのコマンド実行を許可", tl_cron:"Cronジョブ有効", tl_web_fetch:"Webフェッチ有効",
tl_read_file:"ファイル読取有効", tl_write_file:"ファイル書込有効",
tl_skills:"スキル有効", tl_mcp:"MCP有効", tl_subagent:"サブエージェント有効",
lbl_api_key:"APIキー", lbl_base_url:"Base URL", ph_api_key:"APIキーを貼り付け", ph_optional:"任意",
save_confirm:"設定を保存してPicoClawを再起動しますか",
weixin_guide_title:"WeChat個人アカウント", weixin_not_connected:"WeChat未接続",
weixin_authorized_not_enabled:"認証済み、チャンネル無効", weixin_go_enable:"有効化",
weixin_connected:"WeChat接続済み"
},
pt: {
version_info:"Versão", current_version:"Versão Atual", build_time:"Build", latest_ver_label:"Última Versão",
latest_version:"Atualizado", update_available:"Nova versão disponível", update_now:"Atualizar",
confirm_update:"Atualizar PicoClaw? O serviço será reiniciado.",
check_failed:"Verificando...", service_status:"Status do Serviço", running:"Executando", stopped:"Parado",
service_not_running:"Serviço parado", start:"Iniciar", stop:"Parar", restart:"Reiniciar", normal:"OK", abnormal:"Erro",
autostart_on:"Início automático: ON", autostart_off:"Início automático: OFF",
memory_mb:"Memória (MB)", process_id:"Process ID", port_18790:"Porta 18790",
tab_channels:"Canais", tab_config:"Editor de Config", tab_logs:"Logs do Sistema",
select_mode:"Modo:", mode_form:"Formulário", mode_json:"JSON",
save_restart:"Salvar & Reiniciar", reset_form:"Resetar", format_json:"Formatar", sync_from_form:"Sincronizar",
no_logs:"Sem logs", refresh_logs:"Atualizar",
enabled:"Ativado", disabled:"Desativado", no_channels:"Nenhum canal",
sec_ai_model:"Config IA", lbl_provider:"Provedor IA", lbl_model:"Modelo",
sec_channels:"Canais", sec_providers:"API Keys", sec_system:"Sistema", sec_tools:"Ferramentas",
confirm_save:"Salvar e reiniciar?", json_error:"Erro JSON: ",
ch_feishu:"Feishu", ch_telegram:"Telegram", ch_discord:"Discord", ch_wecom:"WeCom Bot",
ch_weixin:"WeChat", ch_slack:"Slack", ch_qq:"QQ", ch_line:"LINE", ch_dingtalk:"DingTalk",
ch_whatsapp:"WhatsApp", ch_maixcam:"MaixCam", ch_wechat_work:"WeCom",
weixin_guide_title:"WeChat", weixin_not_connected:"WeChat não conectado",
weixin_authorized_not_enabled:"Autorizado mas desativado", weixin_go_enable:"Ativar",
weixin_connected:"WeChat Conectado", save_confirm:"Salvar config e reiniciar?"
},
de: {
version_info:"Versionsinfo", current_version:"Aktuelle Version", build_time:"Build-Zeit", latest_ver_label:"Neueste Version",
latest_version:"Aktuell", update_available:"Neue Version verfügbar", update_now:"Aktualisieren",
confirm_update:"PicoClaw aktualisieren? Dienst wird kurz neu gestartet.",
check_failed:"Prüfe Updates...", service_status:"Dienststatus", running:"Läuft", stopped:"Gestoppt",
service_not_running:"Dienst läuft nicht", start:"Start", stop:"Stop", restart:"Neustart", normal:"OK", abnormal:"Fehler",
autostart_on:"Autostart: AN", autostart_off:"Autostart: AUS",
memory_mb:"Speicher (MB)", process_id:"Prozess-ID", port_18790:"Port 18790",
tab_channels:"Kanäle", tab_config:"Konfig Editor", tab_logs:"Systemprotokolle",
select_mode:"Modus:", mode_form:"Formular", mode_json:"JSON",
save_restart:"Speichern & Neustart", reset_form:"Zurücksetzen", format_json:"Formatieren", sync_from_form:"Vom Formular",
no_logs:"Keine Logs", refresh_logs:"Aktualisieren",
enabled:"Aktiviert", disabled:"Deaktiviert", no_channels:"Keine Kanäle",
sec_ai_model:"KI-Modell", lbl_provider:"KI-Anbieter", lbl_model:"Modellname",
sec_channels:"Kanalverwaltung", sec_providers:"API-Schlüssel", sec_system:"Systemeinstellungen", sec_tools:"Werkzeuge",
confirm_save:"Konfiguration speichern und neustarten?", json_error:"JSON Fehler: ",
ch_feishu:"Feishu", ch_telegram:"Telegram", ch_discord:"Discord", ch_wecom:"WeCom Bot",
ch_weixin:"WeChat", ch_slack:"Slack", ch_qq:"QQ", ch_line:"LINE", ch_dingtalk:"DingTalk",
ch_whatsapp:"WhatsApp", ch_maixcam:"MaixCam", ch_wechat_work:"WeCom",
weixin_guide_title:"WeChat", weixin_not_connected:"WeChat nicht verbunden",
weixin_authorized_not_enabled:"Autorisiert, Kanal deaktiviert", weixin_go_enable:"Aktivieren",
weixin_connected:"WeChat Verbunden", save_confirm:"Konfiguration speichern und neustarten?"
}
};
function I(key, fallback) {
return (T[_curLang] && T[_curLang][key]) || T.zh[key] || fallback || key;
}
function setLang(lang) {
_curLang = lang;
localStorage.setItem('pc_lang', lang);
document.querySelectorAll('.lang-btn').forEach(function(b) {
b.classList.toggle('act', b.textContent.trim() === {zh:'中文',en:'EN',ja:'日本語',pt:'PT',de:'DE'}[lang]);
});
applyI18n();
}
function applyI18n() {
document.querySelectorAll('[data-i18n]').forEach(function(el) {
var key = el.getAttribute('data-i18n');
var val = I(key);
if (val) {
var small = el.querySelector('small');
if (small) {
var nodes = el.childNodes;
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].nodeType === 3) { nodes[i].textContent = val + ' '; break; }
}
} else {
el.textContent = val;
}
}
});
buildChannelsList();
buildChannelsForm();
buildProvidersForm();
buildSystemForm();
buildToolsForm();
loadFormFromJSON();
}
// ========== Channel definitions ==========
var CHANNELS = [
{key:'feishu', fields:[{path:'channels.feishu.app_id',ph:'App ID',sm:true},{path:'channels.feishu.app_secret',ph:'App Secret',sm:true,pw:true}]},
{key:'telegram', fields:[{path:'channels.telegram.base_url',ph:'Base URL (optional)'},{path:'channels.telegram.proxy',ph:'Proxy (optional)'}]},
{key:'discord', fields:[]},
{key:'wechat_work', fields:[]},
{key:'weixin', fields:[{path:'channels.weixin.base_url',ph:'Base URL'},{path:'channels.weixin.cdn_base_url',ph:'CDN Base URL'},{path:'channels.weixin.proxy',ph:'Proxy'}]},
{key:'wecom', fields:[{path:'channels.wecom.bot_id',ph:'Bot ID',sm:true}]},
{key:'slack', fields:[]},
{key:'qq', fields:[{path:'channels.qq.app_id',ph:'App ID',sm:true}]},
{key:'dingtalk', fields:[]},
{key:'whatsapp', fields:[]},
{key:'line', fields:[]},
{key:'maixcam', fields:[]}
];
function buildChannelsList() {
var html = '';
var found = false;
CHANNELS.forEach(function(ch) {
var enabled = getByPath(_origConfig, 'channels.' + ch.key + '.enabled');
if (enabled === undefined) return;
found = true;
var cls = enabled ? 'badge-on' : 'badge-off';
var txt = enabled ? I('enabled') : I('disabled');
html += '<div class="ch-row"><span class="ch-name">' + I('ch_' + ch.key, ch.key) + '</span><span class="badge ' + cls + '">' + txt + '</span></div>';
});
document.getElementById('channels-list').innerHTML = found ? html : '<div style="padding:20px;text-align:center;color:#888">' + I('no_channels') + '</div>';
}
function buildChannelsForm() {
var html = '';
CHANNELS.forEach(function(ch) {
html += '<div class="cfg-row"><div class="cfg-label">' + I('ch_' + ch.key, ch.key) + '</div><div class="cfg-field">';
html += '<div class="toggle-wrap"><label class="toggle"><input type="checkbox" data-path="channels.' + ch.key + '.enabled" data-type="bool"><span class="slider"></span></label>';
html += '<span class="toggle-label">' + (I('tl_' + ch.key, '') || ch.key) + '</span></div>';
if (ch.fields.length) {
html += '<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">';
ch.fields.forEach(function(f) {
var cls = 'cfg-input' + (f.sm ? ' cfg-input-sm' : '');
html += '<input type="' + (f.pw ? 'password' : 'text') + '" class="' + cls + '" data-path="' + f.path + '" placeholder="' + f.ph + '">';
});
html += '</div>';
}
html += '</div></div>';
});
document.getElementById('channels-form-body').innerHTML = html;
bindToggles();
}
function buildProvidersForm() {
var providers = [
{id:'zhipu',url:'https://open.bigmodel.cn/api/paas/v4/'},
{id:'openai',url:'https://api.openai.com/v1/'},
{id:'chatgpt',url:'https://api.openai.com/v1/'},
{id:'claude',url:'https://api.anthropic.com/'},
{id:'deepseek',url:'https://api.deepseek.com/'},
{id:'anthropic',url:'https://api.anthropic.com/'}
];
var html = '';
providers.forEach(function(p) {
html += '<div class="cfg-row"><div class="cfg-label">' + p.id + ' ' + I('lbl_api_key') + '</div>';
html += '<div class="cfg-field"><input type="password" class="cfg-input" data-path="providers.' + p.id + '.api_key" placeholder="' + I('ph_api_key') + '"></div></div>';
html += '<div class="cfg-row"><div class="cfg-label">' + p.id + ' ' + I('lbl_base_url') + '<small>' + I('ph_optional') + '</small></div>';
html += '<div class="cfg-field"><input type="text" class="cfg-input" data-path="providers.' + p.id + '.base_url" placeholder="' + p.url + '"></div></div>';
});
html += '<div class="cfg-row"><div class="cfg-label">Ollama ' + I('lbl_base_url') + '</div>';
html += '<div class="cfg-field"><input type="text" class="cfg-input" data-path="providers.ollama.base_url" placeholder="http://localhost:11434/"></div></div>';
document.getElementById('providers-form-body').innerHTML = html;
}
function buildSystemForm() {
var html = '';
html += '<div class="cfg-row"><div class="cfg-label">' + I('lbl_listen') + '<small>listen</small></div>';
html += '<div class="cfg-field"><input type="text" class="cfg-input cfg-input-sm" data-path="listen" placeholder="0.0.0.0:18790"></div></div>';
html += '<div class="cfg-row"><div class="cfg-label">' + I('lbl_heartbeat') + '</div>';
html += '<div class="cfg-field"><div class="toggle-wrap"><label class="toggle"><input type="checkbox" data-path="heartbeat.enabled" data-type="bool"><span class="slider"></span></label><span class="toggle-label">' + I('tl_heartbeat') + '</span></div></div></div>';
html += '<div class="cfg-row"><div class="cfg-label">' + I('lbl_heartbeat_interval') + '</div>';
html += '<div class="cfg-field"><input type="number" class="cfg-input cfg-input-xs" data-path="heartbeat.interval" min="5" max="300"></div></div>';
html += '<div class="cfg-row"><div class="cfg-label">' + I('lbl_media_cleanup') + '</div>';
html += '<div class="cfg-field"><div class="toggle-wrap"><label class="toggle"><input type="checkbox" data-path="media_cleanup.enabled" data-type="bool"><span class="slider"></span></label><span class="toggle-label">' + I('tl_media_cleanup') + '</span></div></div></div>';
html += '<div class="cfg-row"><div class="cfg-label">' + I('lbl_session_scope') + '</div>';
html += '<div class="cfg-field"><select class="cfg-input cfg-input-sm" data-path="session.dm_scope">';
html += '<option value="per-channel-peer">' + I('scope_per_peer') + '</option>';
html += '<option value="per-channel">' + I('scope_per_channel') + '</option>';
html += '<option value="global">' + I('scope_global') + '</option>';
html += '</select></div></div>';
document.getElementById('system-form-body').innerHTML = html;
bindToggles();
}
function buildToolsForm() {
var rows = [
{lbl:'lbl_exec',path:'tools.exec.enabled',tl:'tl_exec'},
{lbl:'lbl_exec_timeout',path:'tools.exec.timeout_seconds',type:'number'},
{lbl:'lbl_exec_remote',path:'tools.exec.allow_remote',tl:'tl_exec_remote'},
{lbl:'lbl_cron',path:'tools.cron.enabled',tl:'tl_cron'},
{lbl:'lbl_cron_timeout',path:'tools.cron.exec_timeout_minutes',type:'number'},
{lbl:'lbl_web_fetch',path:'tools.web_fetch.enabled',tl:'tl_web_fetch'}
];
var html = '';
rows.forEach(function(r) {
html += '<div class="cfg-row"><div class="cfg-label">' + I(r.lbl) + '</div><div class="cfg-field">';
if (r.tl) {
html += '<div class="toggle-wrap"><label class="toggle"><input type="checkbox" data-path="' + r.path + '" data-type="bool"><span class="slider"></span></label><span class="toggle-label">' + I(r.tl) + '</span></div>';
} else {
html += '<input type="number" class="cfg-input cfg-input-xs" data-path="' + r.path + '" min="1" max="600">';
}
html += '</div></div>';
});
html += '<div class="cfg-row"><div class="cfg-label">' + I('lbl_file_rw') + '</div><div class="cfg-field">';
html += '<div class="toggle-wrap"><label class="toggle"><input type="checkbox" data-path="tools.read_file.enabled" data-type="bool"><span class="slider"></span></label><span class="toggle-label">' + I('tl_read_file') + '</span></div>';
html += '<div class="toggle-wrap" style="margin-left:16px"><label class="toggle"><input type="checkbox" data-path="tools.write_file.enabled" data-type="bool"><span class="slider"></span></label><span class="toggle-label">' + I('tl_write_file') + '</span></div>';
html += '</div></div>';
[['lbl_skills','tools.skills.enabled','tl_skills'],['lbl_mcp','tools.mcp.enabled','tl_mcp'],['lbl_subagent','tools.subagent.enabled','tl_subagent']].forEach(function(r) {
html += '<div class="cfg-row"><div class="cfg-label">' + I(r[0]) + '</div><div class="cfg-field">';
html += '<div class="toggle-wrap"><label class="toggle"><input type="checkbox" data-path="' + r[1] + '" data-type="bool"><span class="slider"></span></label><span class="toggle-label">' + I(r[2]) + '</span></div>';
html += '</div></div>';
});
document.getElementById('tools-form-body').innerHTML = html;
bindToggles();
}
// ========== Core functions ==========
function switchTab(name, el) {
document.querySelectorAll('.pc-tab').forEach(function(t){t.classList.remove('act')});
document.querySelectorAll('.pc-panel').forEach(function(p){p.classList.remove('act')});
el.classList.add('act');
document.getElementById('panel-'+name).classList.add('act');
}
function switchCfgMode(mode, el) {
document.querySelectorAll('.cfg-mode-btn').forEach(function(b){b.classList.remove('act')});
el.classList.add('act');
if (mode === 'form') {
document.getElementById('cfg-form-mode').style.display = '';
document.getElementById('cfg-json-mode').style.display = 'none';
loadFormFromJSON();
} else {
document.getElementById('cfg-form-mode').style.display = 'none';
document.getElementById('cfg-json-mode').style.display = '';
loadJSONFromForm();
}
}
function toggleSection(head) { head.parentElement.classList.toggle('closed'); }
function getByPath(obj, path) {
var parts = path.split('.'), cur = obj;
for (var i = 0; i < parts.length; i++) {
if (cur == null || typeof cur !== 'object') return undefined;
cur = cur[parts[i]];
}
return cur;
}
function setByPath(obj, path, val) {
var parts = path.split('.'), cur = obj;
for (var i = 0; i < parts.length - 1; i++) {
if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
cur = cur[parts[i]];
}
cur[parts[parts.length-1]] = val;
}
function loadFormFromJSON() {
document.querySelectorAll('#cfg-form-mode [data-path]').forEach(function(el) {
var path = el.getAttribute('data-path');
var val = getByPath(_origConfig, path);
if (val === undefined) return;
var type = el.getAttribute('data-type');
if (type === 'bool') el.checked = !!val;
else if (el.tagName === 'SELECT') el.value = val;
else if (el.tagName === 'INPUT') el.value = val;
});
updateToggleLabels();
}
function loadJSONFromForm() {
var obj = JSON.parse(JSON.stringify(_origConfig));
collectFormValues(obj);
var editor = document.getElementById('json-editor');
if (editor) editor.value = JSON.stringify(obj, null, 2);
}
function collectFormValues(obj) {
document.querySelectorAll('#cfg-form-mode [data-path]').forEach(function(el) {
var path = el.getAttribute('data-path');
var type = el.getAttribute('data-type');
var val;
if (type === 'bool') val = el.checked;
else if (el.type === 'number') val = parseInt(el.value) || 0;
else val = el.value;
if (val !== '' && val !== undefined) setByPath(obj, path, val);
});
return obj;
}
function formatJSON() {
var editor = document.getElementById('json-editor');
try { var obj = JSON.parse(editor.value); editor.value = JSON.stringify(obj, null, 2); }
catch(e) { alert(I('json_error') + e.message); }
}
function saveFormConfig() {
var obj = JSON.parse(JSON.stringify(_origConfig));
collectFormValues(obj);
document.getElementById('form-config-json').value = JSON.stringify(obj, null, 2);
if (confirm(I('save_confirm'))) document.getElementById('config-form').submit();
}
function updateToggleLabels() {
document.querySelectorAll('.toggle-wrap').forEach(function(wrap) {
var cb = wrap.querySelector('input[type="checkbox"]');
var lbl = wrap.querySelector('.toggle-label');
if (cb && lbl) {
lbl.classList.toggle('on', cb.checked);
lbl.classList.toggle('off', !cb.checked);
}
});
}
function bindToggles() {
document.querySelectorAll('#cfg-form-mode .toggle input[type="checkbox"]').forEach(function(cb) {
if (!cb._bound) { cb.addEventListener('change', updateToggleLabels); cb._bound = true; }
});
}
// ========== Init ==========
setLang(_curLang);
</script>
<!-- WeChat Guide Card (bottom of page) -->
<% if weixin_status == "none" then %>
<div class="pc-card" style="border-color:#e3f2fd;background:#f8fbff">
<h3 data-i18n="weixin_guide_title">&#128172; 微信个人号接入</h3>
<div style="display:flex;align-items:center;gap:14px;padding:14px;background:#e3f2fd;border-radius:8px;margin-bottom:12px">
<div style="font-size:28px;flex-shrink:0">&#128247;</div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#1565c0" data-i18n="weixin_not_connected">尚未接入微信</div>
<div style="font-size:12px;color:#1976d2;margin-top:3px;line-height:1.5">
通过 <code style="background:#bbdefb;padding:1px 5px;border-radius:3px;font-size:11px">picoclaw auth weixin</code> 命令扫码授权后,即可通过微信个人号与 AI 助手对话
</div>
</div>
</div>
<div style="font-size:13px;color:#555;line-height:1.8">
<div style="margin-bottom:8px"><strong>接入步骤:</strong></div>
<div style="background:#fff;border-radius:6px;padding:12px 16px;border:1px solid #bbdefb">
<div style="margin-bottom:6px">&#10102; SSH 登录路由器</div>
<div style="margin-bottom:6px">&#10103; 执行命令:<code style="background:#e8eaf6;padding:2px 8px;border-radius:4px">picoclaw auth weixin</code></div>
<div style="margin-bottom:6px">&#10104; 用微信扫描终端显示的二维码</div>
<div style="margin-bottom:6px">&#10105; 授权成功后,回到此处开启通道即可</div>
</div>
</div>
</div>
<% elseif weixin_status == "configured" then %>
<div class="pc-card" style="border-color:#fff3e0;background:#fffbf5">
<h3 data-i18n="weixin_guide_title">&#128172; 微信个人号接入</h3>
<div style="display:flex;align-items:center;gap:14px;padding:14px;background:#fff3e0;border-radius:8px">
<div style="font-size:28px;flex-shrink:0">&#9989;</div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#e65100" data-i18n="weixin_authorized_not_enabled">已授权,但通道未启用</div>
<div style="font-size:12px;color:#ef6c00;margin-top:3px">微信扫码授权已完成,请到「配置编辑 → 通道管理」中开启微信通道</div>
</div>
<button class="pc-btn green" onclick="switchTab('config',document.querySelectorAll('.pc-tab')[1])" data-i18n="weixin_go_enable">前往启用</button>
</div>
</div>
<% elseif weixin_status == "connected" then %>
<div class="pc-card" style="border-color:#e8f5e9;background:#f8fff8">
<h3 data-i18n="weixin_guide_title">&#128172; 微信个人号接入</h3>
<div style="display:flex;align-items:center;gap:14px;padding:14px;background:#e8f5e9;border-radius:8px">
<div style="font-size:28px;flex-shrink:0">&#128994;</div>
<div style="flex:1">
<div style="font-size:14px;font-weight:600;color:#2e7d32" data-i18n="weixin_connected">微信已连接</div>
<div style="font-size:12px;color:#43a047;margin-top:3px">微信个人号通道已启用,可以直接通过微信与 AI 助手对话</div>
</div>
</div>
</div>
<% end %>
<%+footer%>

View file

@ -0,0 +1,195 @@
#!/bin/sh
# ============================================
# luci-app-picoclaw APK Builder for OpenWrt 25.xx
# Run this script ON your OpenWrt 25.x router
# ============================================
#
# Usage:
# chmod +x build-apk.sh
# ./build-apk.sh
#
# Output: luci-app-picoclaw_25.xx_all.apk
set -e
PKG_NAME="luci-app-picoclaw"
PKG_VERSION="1.0.0"
PKG_DESC="LuCI management interface for PicoClaw AI assistant"
PKG_MAINTAINER="GennKann"
PKG_ARCH="all"
OUTPUT_FILE="${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.apk"
BUILD_DIR="/tmp/${PKG_NAME}-build"
echo "=== Building ${PKG_NAME} APK for OpenWrt 25.xx ==="
echo ""
# Check if running on OpenWrt 25 with apk
if command -v apk >/dev/null 2>&1; then
echo "[OK] apk-tools found"
PKG_MANAGER="apk"
elif command -v opkg >/dev/null 2>&1; then
echo "[WARN] opkg detected. This script is for OpenWrt 25+ (apk)."
echo " Use the IPK package or install script for OpenWrt 24.10."
exit 1
else
echo "[ERROR] Neither apk nor opkg found. Are you on OpenWrt?"
exit 1
fi
# Clean up
rm -rf "${BUILD_DIR}"
mkdir -p "${BUILD_DIR}/pkg"
# --- Install files ---
echo "[1/5] Installing files..."
# LuCI Controller
mkdir -p "${BUILD_DIR}/pkg/usr/lib/lua/luci/controller"
cat > "${BUILD_DIR}/pkg/usr/lib/lua/luci/controller/picoclaw.lua" << 'LUACONTROLLER'
module("luci.controller.picoclaw", package.seeall)
function index()
entry({"admin", "services", "picoclaw"}, call("action_main"), "PicoClaw", 60)
entry({"admin", "services", "picoclaw", "action"}, call("action_do"), nil)
end
function action_main()
local uci = require "luci.model.uci".cursor()
luci.http.prepare_content("text/html")
luci.http.render("picoclaw/main", { })
end
function action_do()
local act = luci.http.formvalue("act")
if act == "start" then
os.execute("/etc/init.d/picoclaw start")
elseif act == "stop" then
os.execute("/etc/init.d/picoclaw stop")
elseif act == "restart" then
os.execute("/etc/init.d/picoclaw restart")
elseif act == "enable" then
os.execute("/etc/init.d/picoclaw enable")
elseif act == "disable" then
os.execute("/etc/init.d/picoclaw disable")
elseif act == "update" then
os.execute("picoclaw update 2>&1")
end
luci.http.redirect(luci.dispatcher.build_url("admin/services/picoclaw"))
end
LUACONTROLLER
echo " -> controller/picoclaw.lua"
# LuCI View
mkdir -p "${BUILD_DIR}/pkg/usr/lib/lua/luci/view/picoclaw"
# The view template is large; we'll copy it via the install script
# For the APK build, we include a placeholder and the full install script will deploy it
echo " (LuCI view template will be deployed by install_picoclaw_luci.py)"
# init.d service script
mkdir -p "${BUILD_DIR}/pkg/etc/init.d"
cat > "${BUILD_DIR}/pkg/etc/init.d/picoclaw" << 'INITD'
#!/bin/sh /etc/rc.common
START=99
STOP=90
USE_PROCD=1
start_service() {
procd_open_instance picoclaw
procd_set_param command /usr/bin/picoclaw gateway
procd_set_param respawn
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param user root
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "picoclaw"
}
INITD
chmod 755 "${BUILD_DIR}/pkg/etc/init.d/picoclaw"
echo " -> etc/init.d/picoclaw"
# --- Build APK package ---
echo "[2/5] Building APK package..."
# Use apk-tools to create the package
# APK package format for OpenWrt 25 uses the apk-tools mkpkg command
cd "${BUILD_DIR}"
# Create the APK package using apk-tools
# The .PKGINFO file contains package metadata
mkdir -p pkg/.PKGINFO
cat > pkg/.PKGINFO/pkginfo << PKGINFO
# Generated by luci-app-picoclaw build script
pkgname = ${PKG_NAME}
pkgver = ${PKG_VERSION}
pkgdesc = ${PKG_DESC}
url = https://github.com/GennKann/luci-app-picoclaw
builddate = $(date +%s)
packager = ${PKG_MAINTAINER}
size = $(du -sb pkg | cut -f1)
arch = ${PKG_ARCH}
origin = ${PKG_NAME}
license = MIT
depend = luci
depend = picoclaw
datahash = $(find pkg -type f -exec sha256sum {} \; | sort | sha256sum | cut -d' ' -f1)
PKGINFO
echo " -> .PKGINFO/pkginfo"
# Try to use apk-tools mkpkg if available
if command -v apk >/dev/null 2>&1; then
if apk mkpkg --help >/dev/null 2>&1; then
echo "[3/5] Using apk mkpkg..."
cd "${BUILD_DIR}/pkg"
apk mkpkg -o "/tmp/${OUTPUT_FILE}" 2>/dev/null || true
if [ -f "/tmp/${OUTPUT_FILE}" ]; then
echo "[4/5] APK created successfully!"
cp "/tmp/${OUTPUT_FILE}" "/root/${OUTPUT_FILE}" 2>/dev/null || \
cp "/tmp/${OUTPUT_FILE}" "$(pwd)/${OUTPUT_FILE}"
echo "[5/5] Package saved to: /root/${OUTPUT_FILE}"
echo ""
echo "Install with:"
echo " apk add --allow-untrusted /root/${OUTPUT_FILE}"
rm -rf "${BUILD_DIR}"
exit 0
fi
fi
fi
# Fallback: create a simple tar.gz based APK (compatible structure)
echo "[3/5] Using fallback tar-based APK build..."
cd "${BUILD_DIR}"
tar czf "/tmp/${OUTPUT_FILE}" -C pkg .
if [ -f "/tmp/${OUTPUT_FILE}" ]; then
echo "[4/5] Package archive created!"
# Copy to accessible location
cp "/tmp/${OUTPUT_FILE}" "/root/${OUTPUT_FILE}" 2>/dev/null || \
cp "/tmp/${OUTPUT_FILE}" "$(pwd)/${OUTPUT_FILE}"
echo "[5/5] Package saved!"
echo ""
echo "=== Build Complete ==="
echo "File: /root/${OUTPUT_FILE}"
echo ""
echo "Install on this router:"
echo " apk add --allow-untrusted /root/${OUTPUT_FILE}"
echo ""
echo "Or copy to another router and install:"
echo " scp /root/${OUTPUT_FILE} root@<router-ip>:/tmp/"
echo " ssh root@<router-ip> 'apk add --allow-untrusted /tmp/${OUTPUT_FILE}'"
echo ""
echo "NOTE: The LuCI view template is large. For the full management"
echo "interface with status monitoring, config editing, and 5-language"
echo "support, also run the install_picoclaw_luci.py script from:"
echo " https://github.com/GennKann/luci-app-picoclaw"
else
echo "[ERROR] Failed to create package!"
exit 1
fi
# Cleanup
rm -rf "${BUILD_DIR}"

View file

@ -0,0 +1,19 @@
#!/bin/sh /etc/rc.common
START=99
USE_PROCD=1
start_service() {
procd_open_instance
procd_set_param command /usr/bin/picoclaw gateway
procd_set_param env HOME=/root
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param user root
procd_close_instance
}
stop_service() {
killall picoclaw 2>/dev/null
}