feat: add LuCI web management interface for OpenWrt
This commit is contained in:
parent
1809d04905
commit
2f1b196aea
15 changed files with 2414 additions and 0 deletions
66
contrib/luci-app-picoclaw/LICENSE
Normal file
66
contrib/luci-app-picoclaw/LICENSE
Normal 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.
|
||||
161
contrib/luci-app-picoclaw/README.de.md
Normal file
161
contrib/luci-app-picoclaw/README.de.md
Normal 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 |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  | — |
|
||||
|
||||
## ✨ 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.10(opkg / .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.xx(apk / .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>
|
||||
172
contrib/luci-app-picoclaw/README.ja.md
Normal file
172
contrib/luci-app-picoclaw/README.ja.md
Normal 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>
|
||||
|
||||
---
|
||||
|
||||
## 📸 スクリーンショット
|
||||
|
||||
| デスクトップ | モバイル |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  | — |
|
||||
|
||||
## ✨ 機能
|
||||
|
||||
- 🖥️ **モダンダッシュボード** — サービス状態、PID、メモリ使用量、ポート監視をリアルタイム表示
|
||||
- 🚀 **サービス制御** — PicoClawの開始/停止/再起動をワンクリック
|
||||
- ⚡ **自動起動切替** — Web UIから起動時自動開始の有効/無効を切り替え
|
||||
- 📡 **チャンネル管理** — 全チャンネルの表示・管理(Feishu、Telegram、Discord、WeChat、WeCom、Slack、QQ、LINE、DingTalk、WhatsApp、MaixCam)
|
||||
- 🔧 **フォーム設定エディタ** — 直感的なフォームUIでAIモデル、プロバイダー、システム設定、ツールを設定
|
||||
- 📝 **JSON設定エディタ** — JSON直接編集、フォーマット、バリデーション対応
|
||||
- 🌍 **5言語対応** — 組み込みi18n:English、简体中文、日本語、Português、Deutsch
|
||||
- 🔄 **オンライン更新** — 新バージョンの確認とUIからの直接更新
|
||||
- 📊 **システムログ** — PicoClawシステムログをリアルタイム表示
|
||||
- 🔑 **マルチプロバイダー対応** — Zhipu、OpenAI、ChatGPT、Claude、DeepSeek、Anthropic、Ollama、Azure OpenAIのAPIキー設定
|
||||
- 🎨 **レスポンシブデザイン** — デスクトップとモバイルブラウザの両方に対応
|
||||
|
||||
## 📋 動作要件
|
||||
|
||||
| 要件 | 詳細 |
|
||||
|---|---|
|
||||
| **OpenWrt** | 24.10 / 25.xx(LuCI導入済み) |
|
||||
| **アーキテクチャ** | 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.10(opkg / .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.xx(apk / .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>
|
||||
69
contrib/luci-app-picoclaw/README.md
Normal file
69
contrib/luci-app-picoclaw/README.md
Normal 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 |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  | — |
|
||||
|
||||
## 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)
|
||||
161
contrib/luci-app-picoclaw/README.pt.md
Normal file
161
contrib/luci-app-picoclaw/README.pt.md
Normal 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 |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  | — |
|
||||
|
||||
## ✨ 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.10(opkg / .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.xx(apk / .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>
|
||||
199
contrib/luci-app-picoclaw/README.zh.md
Normal file
199
contrib/luci-app-picoclaw/README.zh.md
Normal 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>
|
||||
|
||||
---
|
||||
|
||||
## 📸 截图预览
|
||||
|
||||
| 桌面端 | 移动端 |
|
||||
|:---:|:---:|
|
||||
|  |  |
|
||||
|  | — |
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- 🖥️ **现代化仪表盘** — 实时显示服务状态、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.10(opkg / .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.xx(apk / .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 使用 APKv3(Alpine 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>
|
||||
35
contrib/luci-app-picoclaw/Screenshots/logo.svg
Normal file
35
contrib/luci-app-picoclaw/Screenshots/logo.svg
Normal 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 |
BIN
contrib/luci-app-picoclaw/Screenshots/screenshot_config.png
Normal file
BIN
contrib/luci-app-picoclaw/Screenshots/screenshot_config.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
contrib/luci-app-picoclaw/Screenshots/screenshot_main.png
Normal file
BIN
contrib/luci-app-picoclaw/Screenshots/screenshot_main.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
contrib/luci-app-picoclaw/Screenshots/screenshot_mobile.png
Normal file
BIN
contrib/luci-app-picoclaw/Screenshots/screenshot_mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
194
contrib/luci-app-picoclaw/install.py
Normal file
194
contrib/luci-app-picoclaw/install.py
Normal 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)
|
||||
333
contrib/luci-app-picoclaw/luci/controller/picoclaw.lua
Normal file
333
contrib/luci-app-picoclaw/luci/controller/picoclaw.lua
Normal 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("&", "&")
|
||||
s = s:gsub("<", "<")
|
||||
s = s:gsub(">", ">")
|
||||
s = s:gsub('"', """)
|
||||
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
|
||||
810
contrib/luci-app-picoclaw/luci/view/picoclaw/main.htm
Normal file
810
contrib/luci-app-picoclaw/luci/view/picoclaw/main.htm
Normal 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 %>✓<% else %>⚠<% 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">🚧</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">✅</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">🔌</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">⚡ 开机自启: 开</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">⚡ 开机自启: 关</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)">🏠 <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">🤖</span>
|
||||
<h4 data-i18n="sec_ai_model">AI 模型配置</h4>
|
||||
<span class="arrow">▼</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">📡</span>
|
||||
<h4 data-i18n="sec_channels">通道管理</h4>
|
||||
<span class="arrow">▼</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">🔑</span>
|
||||
<h4 data-i18n="sec_providers">API 密钥与提供商</h4>
|
||||
<span class="arrow">▼</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">⚙</span>
|
||||
<h4 data-i18n="sec_system">系统设置</h4>
|
||||
<span class="arrow">▼</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">🔧</span>
|
||||
<h4 data-i18n="sec_tools">工具与功能</h4>
|
||||
<span class="arrow">▼</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">💾 保存并重启</button>
|
||||
<button type="button" class="pc-btn gray" onclick="loadFormFromJSON()" data-i18n="reset_form">🔄 重置</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'))">💾 <span data-i18n="save_restart">保存并重启</span></button>
|
||||
<button type="button" class="pc-btn blue" onclick="formatJSON()">📝 <span data-i18n="format_json">格式化</span></button>
|
||||
<button type="button" class="pc-btn gray" onclick="loadJSONFromForm()">🔄 <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">💬 微信个人号接入</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">📷</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">❶ SSH 登录路由器</div>
|
||||
<div style="margin-bottom:6px">❷ 执行命令:<code style="background:#e8eaf6;padding:2px 8px;border-radius:4px">picoclaw auth weixin</code></div>
|
||||
<div style="margin-bottom:6px">❸ 用微信扫描终端显示的二维码</div>
|
||||
<div style="margin-bottom:6px">❹ 授权成功后,回到此处开启通道即可</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">💬 微信个人号接入</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">✅</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">💬 微信个人号接入</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">🟢</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%>
|
||||
195
contrib/luci-app-picoclaw/scripts/build-apk-25xx.sh
Normal file
195
contrib/luci-app-picoclaw/scripts/build-apk-25xx.sh
Normal 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}"
|
||||
19
contrib/luci-app-picoclaw/scripts/picoclaw.init
Normal file
19
contrib/luci-app-picoclaw/scripts/picoclaw.init
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue