feat: v0.6.0 — 公益AI接口 + Agent灵魂借尸还魂 + 知识库 + 全局AI诊断 + 官网改版

This commit is contained in:
晴天
2026-03-07 19:36:25 +08:00
parent b09f48f0dd
commit 0752dc2a71
55 changed files with 4346 additions and 480 deletions

View File

@@ -5,6 +5,25 @@
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [0.6.0] - 2026-03-07
### 新功能 (Features)
- **公益 AI 接口计划** — 内置免费 AI 接口gpt.qt.coolGPT-5 全系列模型一键接入Token 费用由项目组承担
- **Agent 灵魂借尸还魂** — AI 助手可从 OpenClaw Agent 加载完整灵魂SOUL / IDENTITY / USER / AGENTS / TOOLS继承人格与记忆
- **知识库注入** — 自定义 Markdown 知识注入 AI 助手,对话时自动激活
- **AI 工具权限管控** — 工具调用权限三档可调(完整 / 受限 / 禁用),危险操作二次确认
- **全局 AI 浮动按钮** — 任意页面错误自动捕获,一键跳转 AI 助手分析诊断
- **一键部署脚本** — `deploy.sh` 支持 curl/wget 双模式,适配 Docker / WSL / Linux 环境
### 改进 (Improvements)
- **安装失败诊断增强** — Rust 后端收集 stderr 最后 15 行JS 端延迟 150ms 确保完整日志捕获;新增 ENOENT(-4058)、权限、网络等详细诊断
- **UI 图标统一** — 全面替换 emoji 为 SVG 图标组件assistant / chat-debug / about / services 等页面)
- **模型配置增强** — 公益接口 Banner + 一键添加全部模型,批量连通性测试
- **官网全面改版** — Hero 换为 AI 助手、Showcase 8 行 + Gallery 6 格重新编排、全部文案重写、新增活动板块和抖音社群
- **开发模式增强** — dev-api.js Mock API 大幅扩展,支持 AI 助手全流程调试
## [0.5.6] - 2026-03-06
### 安全修复 (Security)

View File

@@ -127,77 +127,77 @@ docker run -d --name clawpanel --restart unless-stopped \
</p>
<p align="center">
<img src="docs/00.png" width="800" alt="AI 助手">
<img src="docs/01.png" width="800" alt="AI 助手">
</p>
<p align="center"><em>🤖 AI 助手 — 8 大技能卡片一键触发配置检查、Gateway 诊断、环境检测、一键排障等常用操作</em></p>
<p align="center">
<img src="docs/11.png" width="800" alt="AI 助手工具调用实战">
<img src="docs/00.png" width="800" alt="仪表盘">
</p>
<p align="center"><em>🔧 AI 实战 — 自动调用工具:获取系统信息 → 列出目录 → 读取配置 → 生成健康检查报告,全程可视化</em></p>
<p align="center"><em>仪表盘 — Gateway / 隧道 / 服务实时状态版本信息、Agent 数量、模型池一屏掌握</em></p>
<p align="center">
<img src="docs/12.png" width="800" alt="AI 助手设置">
<img src="docs/02.png" width="800" alt="AI 助手设置 — 公益 AI 接口">
</p>
<p align="center"><em>⚙️ AI 设置 — 独立模型配置,支持任意 OpenAI 兼容 API无需安装 OpenClaw 也能使用 AI 助手</em></p>
<p align="center"><em>⚙️ AI 设置 — 独立模型配置 + 公益 AI 接口一键接入GPT-5 全系列免费可用</em></p>
<p align="center">
<img src="docs/13.png" width="800" alt="AI 图片识别">
<img src="docs/05.png" width="800" alt="AI 助手人设 — Agent 灵魂">
</p>
<p align="center"><em>🖼️ 图片识别 — 粘贴截图或拖拽图片AI 自动识别分析内容,多模态图文混排对话</em></p>
<p align="center"><em><EFBFBD> 借尸还魂 — 从 OpenClaw Agent 加载灵魂SOUL / IDENTITY / USER / AGENTS / TOOLS继承人格与记忆</em></p>
<p align="center">
<img src="docs/01.png" width="800" alt="仪表盘">
<img src="docs/07.png" width="800" alt="实时聊天">
</p>
<p align="center"><em>仪表盘 — 系统运行概览,服务状态一目了然</em></p>
<p align="center"><em>实时聊天 — WebSocket 流式对话,多 Provider 模型自动聚合,支持多模态</em></p>
<p align="center">
<img src="docs/02.png" width="800" alt="实时聊天">
<img src="docs/09.png" width="800" alt="模型配置">
</p>
<p align="center"><em>实时聊天 — WebSocket 流式对话,支持 Markdown 渲染与多会话管理</em></p>
<p align="center"><em>模型配置 — 多服务商统一管理,公益接口一键添加全部模型,主模型+备选自动切换</em></p>
<p align="center">
<img src="docs/05.png" width="800" alt="模型配置">
<img src="docs/13.png" width="800" alt="记忆文件">
</p>
<p align="center"><em>模型配置 — 多服务商管理,主模型+备选自动切换</em></p>
<p align="center">
<img src="docs/08.png" width="800" alt="记忆文件">
</p>
<p align="center"><em>记忆文件 — 在线编辑 Agent 核心配置与工作记忆</em></p>
<p align="center"><em>记忆文件 — 工作记忆、记忆归档、核心文件在线编辑,多 Agent 记忆隔离</em></p>
<details>
<summary><strong>查看更多截图</strong></summary>
<p align="center">
<img src="docs/06.png" width="800" alt="Agent 管理">
<img src="docs/10.png" width="800" alt="Agent 管理">
</p>
<p align="center"><em>Agent 管理 — 多 Agent 创建、身份配置与工作区管理</em></p>
<p align="center"><em>Agent 管理 — 多 Agent 创建、身份配置与独立工作区管理</em></p>
<p align="center">
<img src="docs/07.png" width="800" alt="Gateway 配置">
<img src="docs/11.png" width="800" alt="Gateway 安全认证">
</p>
<p align="center"><em>Gateway 配置 — 端口、访问权限、认证方式可视化配置</em></p>
<p align="center"><em>Gateway — Token / 密码双认证Agent 工具权限三档管控,会话可见性控制</em></p>
<p align="center">
<img src="docs/03.png" width="800" alt="服务管理">
</p>
<p align="center"><em>服务管理 — 启停控制、版本检测、一键升级、配置备份</em></p>
<p align="center"><em>服务管理 — 启停控制、版本检测、一键升级、npm 源切换、配置备份</em></p>
<p align="center">
<img src="docs/04.png" width="800" alt="日志查看">
<img src="docs/12.png" width="800" alt="安全设置">
</p>
<p align="center"><em>日志查看 — 多日志源实时查看与关键词搜索</em></p>
<p align="center"><em>安全设置 — 访问密码保护与无视风险模式</em></p>
<p align="center">
<img src="docs/09.png" width="800" alt="扩展工具">
<img src="docs/14.png" width="800" alt="扩展工具">
</p>
<p align="center"><em>扩展工具 — cftunnel 内网穿透、ClawApp 移动客户端管理</em></p>
<p align="center"><em>扩展工具 — cftunnel 内网穿透、ClawApp 移动客户端一键安装</em></p>
<p align="center">
<img src="docs/10.png" width="800" alt="系统诊断">
<img src="docs/15.png" width="800" alt="系统诊断">
</p>
<p align="center"><em>系统诊断 — 全面健康检测一键修复</em></p>
<p align="center"><em>系统诊断 — 全面健康检测、WebSocket 测试、一键修复配对</em></p>
<p align="center">
<img src="docs/16.png" width="800" alt="关于">
</p>
<p align="center"><em>关于 — 版本信息、社群入口QQ / 微信 / 抖音)、相关项目链接</em></p>
</details>

85
deploy.sh Normal file
View File

@@ -0,0 +1,85 @@
#!/bin/bash
# ClawPanel Web 版一键部署脚本
# 适用于 WSL / Docker / 远程服务器
# 用法: curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash
set -e
REPO="qingchencloud/clawpanel"
INSTALL_DIR="$HOME/.clawpanel-web"
PORT="${CLAWPANEL_PORT:-9099}"
echo ""
echo " ClawPanel Web 版 一键部署脚本"
echo " =============================="
echo ""
# ── 工具函数 ──
fetch() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1"
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$1"
else
echo "❌ 需要 curl 或 wget请先安装"; exit 1
fi
}
download() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL -o "$2" "$1"
elif command -v wget >/dev/null 2>&1; then
wget -qO "$2" "$1"
fi
}
# ── 检查依赖 ──
echo "[1/5] 检查依赖..."
command -v node >/dev/null 2>&1 || { echo "❌ 需要 Node.js请先安装: https://nodejs.org/"; exit 1; }
command -v npm >/dev/null 2>&1 || { echo "❌ 需要 npm"; exit 1; }
echo " node $(node -v) / npm $(npm -v)"
# ── 获取最新版本号 ──
echo "[2/5] 获取最新版本..."
LATEST=$(fetch "https://api.github.com/repos/$REPO/releases/latest" 2>/dev/null | grep '"tag_name"' | sed -E 's/.*"v?([^"]+)".*/\1/' || echo "")
if [ -z "$LATEST" ]; then
echo " 无法获取最新版本,使用 main 分支"
DOWNLOAD_URL="https://github.com/$REPO/archive/refs/heads/main.tar.gz"
else
echo " 最新版本: v$LATEST"
DOWNLOAD_URL="https://github.com/$REPO/archive/refs/tags/v$LATEST.tar.gz"
fi
# ── 下载并解压 ──
echo "[3/5] 下载源码..."
TMP_FILE=$(mktemp /tmp/clawpanel-XXXXXX.tar.gz)
trap "rm -f $TMP_FILE" EXIT
download "$DOWNLOAD_URL" "$TMP_FILE"
if [ ! -s "$TMP_FILE" ]; then
echo "❌ 下载失败,请检查网络连接"; exit 1
fi
mkdir -p "$INSTALL_DIR"
tar xzf "$TMP_FILE" -C "$INSTALL_DIR" --strip-components=1
echo " 解压到 $INSTALL_DIR"
# ── 安装依赖并构建 ──
echo "[4/5] 安装依赖..."
cd "$INSTALL_DIR"
npm install 2>&1 | tail -1
echo "[5/5] 构建前端..."
npx vite build --mode development 2>&1 | tail -2
echo ""
echo " ==============================="
echo " ClawPanel Web 版部署完成!"
echo " ==============================="
echo ""
echo " 启动: cd $INSTALL_DIR && npx serve dist -l $PORT"
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")
echo " 访问: http://$IP:$PORT"
echo ""
echo " 提示: 需要本地 OpenClaw Gateway 运行中(默认端口 3456"
echo " 安装: npm i -g @qingchencloud/openclaw-zh"
echo " 启动: openclaw start"
echo ""

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 152 KiB

BIN
docs/14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

BIN
docs/15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
docs/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -0,0 +1,777 @@
# AI 助手功能扩展规划
> 基于现有工具架构TOOL_DEFS + executeTool + getEnabledTools扩展 6 大能力模块。
> 每个模块独立开关,遵循现有的 `_config.tools.xxx` + 设置面板 toggle 模式。
---
## 当前架构概览
```
TOOL_DEFS = {
system: [get_system_info] // 始终可用
process: [list_processes, check_port] // 始终可用
interaction: [ask_user] // 始终可用
terminal: [run_command] // 开关控制
fileOps: [read_file, write_file, list_directory] // 开关控制
}
_config.tools = { terminal: true/false, fileOps: true/false }
```
扩展后:
```
TOOL_DEFS = {
...existing,
docker: [docker_list, docker_exec, docker_logs, wsl_exec] // 新增
webSearch: [web_search, fetch_url] // 新增
ssh: [ssh_exec, ssh_read_file, ssh_write_file] // 新增
knowledge: [search_knowledge] // 新增
}
_config.tools = {
...existing,
docker: false, // 默认关闭
webSearch: false, // 默认关闭
ssh: false, // 默认关闭
knowledge: false, // 默认关闭
}
```
---
## 模块一Docker / WSL 管理工具
### 场景
- 用户的 OpenClaw 可能安装在 Docker 容器或 WSL 中
- 本地检测不到时,帮用户在容器/WSL 内操作
- 查看容器日志、进入容器执行命令、管理容器生命周期
### 工具定义
| 工具名 | 描述 | 参数 | 危险等级 |
|--------|------|------|----------|
| `docker_list` | 列出 Docker 容器 | `filter?`, `all?` | 安全 |
| `docker_exec` | 在容器内执行命令 | `container`, `command` | ⚠️ 危险 |
| `docker_logs` | 查看容器日志 | `container`, `lines?` | 安全 |
| `docker_compose` | 执行 docker-compose 命令 | `action`, `file?`, `service?` | ⚠️ 危险 |
| `wsl_exec` | 在 WSL 内执行命令 | `distro?`, `command` | ⚠️ 危险 |
| `wsl_list` | 列出 WSL 发行版 | — | 安全 |
### 后端实现
```
Tauri (Rust):
- docker_list → Command::new("docker").args(["ps", ...])
- docker_exec → Command::new("docker").args(["exec", container, ...])
- wsl_exec → Command::new("wsl").args(["-d", distro, "-e", ...]) (Windows only)
dev-api.js (Web):
- execSync('docker ps --format json')
- execSync(`docker exec ${container} ${command}`)
- execSync(`wsl -d ${distro} -e ${command}`) (Windows only)
```
### 安全围栏
- `docker_exec` / `wsl_exec` 归入 DANGEROUS_TOOLS
- `docker rm`, `docker rmi`, `docker system prune` 归入 CRITICAL_PATTERNS
### UI 扩展
- 设置面板工具权限 tab 新增 toggle
```
Docker / WSL 工具 — 允许管理容器和 WSL 环境
```
### 内置技能卡片
```js
{
id: 'detect-docker-openclaw',
icon: '🐳',
name: '检测 Docker/WSL 中的 OpenClaw',
desc: '扫描 Docker 容器和 WSL查找 OpenClaw 安装',
tools: ['docker'],
prompt: `请帮我检查 Docker 和 WSL 中是否安装了 OpenClaw。
1. 调用 get_system_info 判断操作系统
2. 用 docker_list 列出所有容器,过滤包含 openclaw/gateway 的
3. 如果是 Windows用 wsl_list 列出 WSL 发行版
4. 对每个 WSL 发行版,用 wsl_exec 执行 "which openclaw" 检测
5. 汇总发现的 OpenClaw 实例及其状态`
}
```
### 优先级:🔴 高(解决用户最常见困惑)
### 工时估算1-2 天
---
## 模块二:联网搜索工具
### 场景
- 用户遇到不常见的错误AI 知识库可能没有
- 搜索 GitHub Issues、文档、Stack Overflow 找到解决方案
- 查找最新版本信息、API 文档等
### 工具定义
| 工具名 | 描述 | 参数 | 危险等级 |
|--------|------|------|----------|
| `web_search` | 联网搜索关键词 | `query`, `max_results?` | 安全 |
| `fetch_url` | 抓取网页内容 | `url` | 安全 |
### 后端实现方案3 选 1
#### 方案 ADuckDuckGo Instant Answer API推荐免费无 Key
```js
// 搜索
const resp = await fetch(`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json`)
// 但 Instant Answer 只返回摘要,不返回搜索结果列表
// 实际搜索需要用 DuckDuckGo HTML 页面解析或第三方库
```
#### 方案 BSearXNG 代理(自托管,最灵活)
```js
// 部署一个 SearXNG 实例,或者用公共实例
const resp = await fetch(`https://searx.example.com/search?q=${query}&format=json`)
```
#### 方案 CJina Reader API推荐搭配使用免费
```js
// 将任意 URL 转为纯文本/Markdown
const resp = await fetch(`https://r.jina.ai/${targetUrl}`)
const text = await resp.text()
```
### 推荐组合
- **搜索**:使用 DuckDuckGo 的 `html.duckduckgo.com/html/?q=xxx` 页面解析结果
- **内容抓取**:使用 Jina Reader `r.jina.ai/URL` 获取纯文本
- 两者都 **免费无 Key**,无需用户配置
### 系统提示词补充
```
## web_search 使用指南
当你无法确定答案或需要最新信息时,可以使用 web_search 搜索互联网。
搜索后,如果需要更多内容,可以用 fetch_url 抓取具体页面。
搜索技巧:
- 加 site:github.com 搜索 GitHub
- 加 site:stackoverflow.com 搜索 StackOverflow
- 搜索错误信息时,用引号包裹关键错误文本
```
### 安全围栏
- 搜索和抓取不涉及破坏性操作,不归入 DANGEROUS_TOOLS
- 但需要网络请求添加超时保护10 秒)
- URL 抓取限制最大内容长度100KB → 截断)
### UI 扩展
```
联网搜索 — 允许搜索互联网和抓取网页内容(需联网)
```
### 优先级:🔴 高(大幅提升问题解决能力)
### 工时估算0.5-1 天
---
## 模块三SSH 远程管理工具
### 场景
- 用户的 OpenClaw 部署在远程服务器上
- 帮用户远程安装、配置、排查 OpenClaw
- 远程查看日志、重启服务、修改配置
### 工具定义
| 工具名 | 描述 | 参数 | 危险等级 |
|--------|------|------|----------|
| `ssh_exec` | 在远程服务器执行命令 | `connection_id`, `command` | ⚠️ 危险 |
| `ssh_read_file` | 读取远程文件 | `connection_id`, `path` | 安全 |
| `ssh_write_file` | 写入远程文件 | `connection_id`, `path`, `content` | ⚠️ 危险 |
### 配置数据结构
```js
_config.sshConnections = [
{
id: 'my-server',
name: '生产服务器',
host: '192.168.1.100',
port: 22,
user: 'root',
authType: 'key', // 'key' | 'password'
keyPath: '~/.ssh/id_rsa',
// password 不存储在 localStorage每次询问或用 keytar 安全存储
}
]
```
### 后端实现
```
Tauri (Rust):
- 使用 ssh2 crate 或调用系统 ssh CLI
- ssh_exec → Command::new("ssh").args(["-p", port, "user@host", command])
- ssh_read_file → ssh + cat
- ssh_write_file → 通过 stdin pipe 写入
dev-api.js (Web):
- 使用 node-ssh 或 ssh2 npm 包
- 或者直接调用 ssh CLI
```
### 设置 UI新增 tab「远程连接」
```
┌─────────────────────────────────────────────┐
│ 模型配置 │ 工具权限 │ 远程连接 │ 助手人设 │
├─────────────────────────────────────────────┤
│ │
│ ┌─ 生产服务器 ──────────────── [编辑] [删] │
│ │ root@192.168.1.100:22 (密钥认证) │
│ └──────────────────────────────────────────│
│ │
│ ┌─ 测试服务器 ──────────────── [编辑] [删] │
│ │ admin@10.0.0.5:22 (密码认证) │
│ └──────────────────────────────────────────│
│ │
│ [+ 添加连接] │
│ │
│ 提示:推荐使用 SSH 密钥认证。 │
│ 生成密钥ssh-keygen -t ed25519 │
│ 复制公钥ssh-copy-id user@host │
│ │
└─────────────────────────────────────────────┘
```
### 安全围栏
- `ssh_exec`, `ssh_write_file` 归入 DANGEROUS_TOOLS
- 密码认证:每次执行时用 ask_user 确认,或使用系统密钥链
- SSH 密钥路径验证:检查文件是否存在
- 关键命令rm -rf, reboot 等)在远程同样走 CRITICAL_PATTERNS
### 系统提示词补充
```
## SSH 远程管理
用户可能配置了远程服务器连接。当操作远程服务器时:
- 先用 ask_user 确认要操作哪个连接
- 远程命令比本地更谨慎,优先使用只读操作
- 修改配置前先备份cp xxx xxx.bak
```
### 内置技能卡片
```js
{
id: 'remote-manage',
icon: '🌐',
name: '远程管理 OpenClaw',
desc: '通过 SSH 连接远程服务器,管理 OpenClaw',
tools: ['ssh', 'fileOps'],
prompt: `请帮我管理远程服务器上的 OpenClaw。
1. 获取系统信息,列出已配置的 SSH 连接
2. 用 ask_user 让我选择要操作的服务器
3. 用 ssh_exec 检查远程 OpenClaw 状态
4. 检查 Gateway 进程和端口
5. 读取远程配置和日志
6. 汇总远程 OpenClaw 状态报告`
}
```
### 优先级:🟡 中(用户量较少但价值极高)
### 工时估算2-3 天
---
## 模块四:知识库 + 灵魂移植(借尸还魂 🔥)
### 核心理念
OpenClaw 的 Agent 有一套完整的**身份系统**,由工作区引导文件定义:
```
~/.openclaw/workspace/ ← Agent 的"灵魂"所在
├── AGENTS.md ← 操作指令、规则、记忆管理方式
├── SOUL.md ← 人设、边界、语气("Who You Are"
├── IDENTITY.md ← 名称、物种、风格、表情符号、头像
├── USER.md ← 用户档案(名字、称呼、时区、偏好)
├── TOOLS.md ← 工具本地笔记SSH 配置、设备名等)
├── HEARTBEAT.md ← 心跳任务清单
├── MEMORY.md ← 精选长期记忆(仅主会话加载)
└── memory/ ← 每日记忆日志
├── 2026-03-04-1609.md
└── ...
~/.openclaw/agents/<agentId>/agent/ ← Agent 的运行时状态
├── models.json ← 模型提供商配置baseUrl + apiKey + models
├── auth-profiles.json ← 认证配置文件
└── auth.json
```
**"借尸还魂"不是复用知识库,而是完整接管 Agent 的灵魂**——
ClawPanel 的 AI 助手直接读取这些文件,像 OpenClaw 一样把它们注入 system prompt
从而变成那个 Agent有他的名字、他的性格、他的记忆、他认识的用户。
### 4A灵魂移植Agent Identity Takeover
#### 工作流程
1. **扫描** `~/.openclaw/workspace/``~/.openclaw/agents/` 目录
2. **发现** 所有可用的 Agent 身份main、test 等)
3. **用户选择** 要附身的 Agent
4. **读取** 该 Agent 的全部引导文件:
- `SOUL.md` → 注入为人设(替换 ClawPanel 助手的默认人设)
- `IDENTITY.md` → 提取名称/表情/风格(替换助手名称和性格描述)
- `USER.md` → 注入用户上下文(知道用户叫什么、偏好什么)
- `AGENTS.md` → 注入操作规则Agent 的行为准则)
- `TOOLS.md` → 注入工具笔记
- `MEMORY.md` → 注入长期记忆
- `memory/` → 注入最近的每日记忆(最近 3 天)
5. **注入**`buildSystemPrompt()` 中,完全替代默认人设
#### 实现
```js
// 新增配置项
_config.soulSource = null // null = 使用 ClawPanel 默认 | 'openclaw:main' | 'openclaw:test' | 'custom'
_config.soulCache = null // 缓存读取的灵魂文件内容
// buildSystemPrompt 改造
function buildSystemPrompt() {
if (_config.soulSource?.startsWith('openclaw:')) {
// 借尸还魂模式:使用 OpenClaw Agent 的灵魂
return buildOpenClawSoulPrompt()
}
// 默认模式:使用 ClawPanel 自带的系统提示词
return buildDefaultPrompt()
}
function buildOpenClawSoulPrompt() {
const soul = _config.soulCache
if (!soul) return buildDefaultPrompt() // fallback
let prompt = ''
// 1. 身份注入
if (soul.identity) {
prompt += `# Identity\n${soul.identity}\n\n`
}
// 2. 灵魂注入(人设、边界、语气)
if (soul.soul) {
prompt += `# Soul\n${soul.soul}\n\n`
}
// 3. 用户上下文
if (soul.user) {
prompt += `# User\n${soul.user}\n\n`
}
// 4. 操作规则
if (soul.agents) {
prompt += `# Operating Instructions\n${soul.agents}\n\n`
}
// 5. 工具笔记
if (soul.tools) {
prompt += `# Tool Notes\n${soul.tools}\n\n`
}
// 6. 长期记忆
if (soul.memory) {
prompt += `# Long-term Memory\n${soul.memory}\n\n`
}
// 7. 最近的每日记忆
if (soul.recentMemories?.length) {
prompt += `# Recent Memory\n`
for (const m of soul.recentMemories) {
prompt += `## ${m.date}\n${m.content}\n\n`
}
}
// 8. 追加 ClawPanel 特有的工具说明(保持工具能力)
prompt += buildToolInstructions()
return prompt
}
```
#### 灵魂加载函数
```js
async function loadOpenClawSoul(agentId = 'main') {
const home = await getHomeDir()
const ws = `${home}/.openclaw/workspace` // 工作区是全局的,不按 agentId 分
const readSafe = async (path) => {
try { return await api.assistantReadFile(path) }
catch { return null }
}
const soul = {
identity: await readSafe(`${ws}/IDENTITY.md`),
soul: await readSafe(`${ws}/SOUL.md`),
user: await readSafe(`${ws}/USER.md`),
agents: await readSafe(`${ws}/AGENTS.md`),
tools: await readSafe(`${ws}/TOOLS.md`),
memory: await readSafe(`${ws}/MEMORY.md`),
recentMemories: [],
}
// 读取最近 3 天的每日记忆
try {
const memDir = await api.assistantListDir(`${ws}/memory`)
const files = memDir.split('\n').filter(f => f.match(/\d{4}-\d{2}-\d{2}/))
const recent = files.sort().slice(-3)
for (const f of recent) {
const content = await readSafe(`${ws}/memory/${f.trim()}`)
if (content) soul.recentMemories.push({ date: f.trim(), content })
}
} catch {}
return soul
}
```
#### UI设置面板「助手人设」Tab 改造
```
┌─────────────────────────────────────────────┐
│ 模型配置 │ 工具权限 │ 知识库 │ 远程连接 │ 人设 │
├─────────────────────────────────────────────┤
│ │
│ 身份来源 │
│ ┌──────────────────────────────────────────│
│ │ ● ClawPanel 默认人设 │ ← 当前默认
│ │ ○ OpenClaw Agent 身份(借尸还魂) │ ← 新增
│ │ ○ 自定义人设 │
│ └──────────────────────────────────────────│
│ │
│ ─── 当选择「OpenClaw Agent」时显示 ──── │
│ │
│ 选择 Agent: [main ▼] │
│ │
│ 📜 灵魂文件预览 │
│ ┌──────────────────────────────────────────│
│ │ SOUL.md ✅ 已加载 (1.6KB) │
│ │ IDENTITY.md ✅ 已加载 (636B) │
│ │ USER.md ✅ 已加载 (237B) │
│ │ AGENTS.md ✅ 已加载 (7.8KB) │
│ │ TOOLS.md ✅ 已加载 (860B) │
│ │ MEMORY.md ❌ 未找到 │
│ │ memory/ 📝 2 个日志文件 │
│ └──────────────────────────────────────────│
│ │
│ [👻 附身!] [🔄 刷新] │
│ │
│ ⚠️ 附身后,助手将使用该 Agent 的人格、 │
│ 记忆和用户偏好。可随时切回默认。 │
│ │
│ ─── 当选择「ClawPanel 默认」时显示 ──── │
│ │
│ 助手名称: [晴辰助手 ] │
│ 助手性格: [________________________] │
│ │
└─────────────────────────────────────────────┘
```
#### 附身后的效果
| 维度 | 默认模式 | 附身模式 |
|------|----------|----------|
| 名称 | "晴辰助手" | IDENTITY.md 中的名称 |
| 性格 | 简洁专业 | SOUL.md 定义的风格 |
| 称呼用户 | "你" | USER.md 中的称呼(如"爸爸" |
| 行为规则 | ClawPanel 内置 | AGENTS.md 的规则体系 |
| 记忆 | 无 | MEMORY.md + 每日记忆 |
| 工具知识 | ClawPanel 内置 | TOOLS.md 的本地笔记 |
| 工具能力 | 保持不变 | 保持 ClawPanel 的工具 |
**关键设计**:附身只替换"灵魂"system prompt**工具能力保持 ClawPanel 的**。
因为 OpenClaw 的工具exec/read/edit/write和 ClawPanel 的工具本质相同,
但 ClawPanel 有独有的 docker/ssh/搜索等扩展工具,这些要保留。
### 4B自定义知识库
在灵魂移植之外,仍然支持用户上传额外的知识文档:
#### 数据存储
```
~/.openclaw/clawpanel-kb/
├── index.json # 知识库索引
├── docs/
│ ├── api-guide.md # 用户上传的文档
│ ├── faq.md
│ └── deploy-notes.txt
└── chunks/ # 分块索引(可选,用于大文档)
└── ...
```
#### 实现方案
**V1简单方案**
- 小文档(<8KB直接全文注入 system prompt 尾部
- 大文档做关键词搜索(正则匹配 + 上下文窗口)
- 总注入 token 上限4000 tokens
- 知识库和灵魂移植可叠加使用
**V2进阶方案**
- embedding 语义搜索
- `search_knowledge` 工具让 AI 按需检索
### 优先级<E7BAA7> 高(灵魂移植是杀手级差异化功能)
### 工时估算:灵魂移植 1-2 天,自定义知识库 V1 额外 1 天
---
## 模块五:模型配置自动导入
### 场景
- 用户已安装 OpenClaw 并配置了模型
- ClawPanel AI 助手需要单独配置模型(目前手动填写)
- 一键从 OpenClaw 配置导入,省去重复配置
### 实现
#### 数据来源(两个层级)
**层级 1全局配置** `~/.openclaw/openclaw.json`
```json
{
"models": {
"providers": {
"shengsuanyun": {
"baseUrl": "http://127.0.0.1:8082/v1",
"apiKey": "sk-xxx",
"api": "openai-completions"
}
}
}
}
```
**层级 2Agent 模型注册表** `~/.openclaw/agents/<agentId>/agent/models.json`
```json
{
"providers": {
"openai": {
"baseUrl": "http://127.0.0.1:8082/v1",
"apiKey": "sk-eB3ybVNFvqB4fGrTUp3F8Lq16QxF7tut",
"api": "openai-completions",
"models": [
{ "id": "gpt-5.4", "name": "gpt-5.4", "contextWindow": 200000, "maxTokens": 8192 },
{ "id": "gpt-5.2-codex", "name": "gpt-5.2-codex", ... }
]
}
}
}
```
**推荐优先读取 Agent 的 models.json**——它有完整的 baseUrl + apiKey + models 列表,
一键就能填充 ClawPanel 助手的配置。
#### 读取逻辑
```js
async function discoverOpenClawModels() {
const home = await getHomeDir()
const results = []
// 1. 扫描所有 Agent 的 models.json
try {
const agents = await api.assistantListDir(`${home}/.openclaw/agents`)
for (const agentId of agents.split('\n').map(s => s.trim()).filter(Boolean)) {
try {
const raw = await api.assistantReadFile(`${home}/.openclaw/agents/${agentId}/agent/models.json`)
const data = JSON.parse(raw)
for (const [providerId, provider] of Object.entries(data.providers || {})) {
results.push({
source: `Agent: ${agentId}`,
providerId,
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
apiType: provider.api === 'openai-completions' ? 'openai' : provider.api,
models: (provider.models || []).map(m => m.id || m.name),
})
}
} catch {}
}
} catch {}
// 2. 读取全局 openclaw.json 作为补充
try {
const raw = await api.assistantReadFile(`${home}/.openclaw/openclaw.json`)
const config = JSON.parse(raw)
for (const [providerId, provider] of Object.entries(config.models?.providers || {})) {
// 去重:如果 Agent models.json 已有相同 providerId跳过
if (!results.find(r => r.providerId === providerId)) {
results.push({
source: '全局配置',
providerId,
baseUrl: provider.baseUrl,
apiKey: provider.apiKey,
apiType: 'openai',
models: [], // 全局配置没有 models 列表
})
}
}
} catch {}
return results
}
```
#### UI模型配置 tab 新增「导入」按钮
```
┌─────────────────────────────────────────────┐
│ API Base URL API 类型 │
│ [________________________] [OpenAI 兼容 ▼] │
│ │
│ API Key [测试] [拉取] [📥 导入] │ ← 新增「导入」按钮
│ [________________________] │
│ │
│ 模型 温度 │
│ [________________________] [0.7] │
│ │
└─────────────────────────────────────────────┘
```
点击「📥 导入」弹出选择面板:
```
┌─────────────────────────────────────────────┐
│ 从 OpenClaw 导入模型配置 │
│ │
│ 检测到以下已配置的服务商: │
│ │
│ ○ OpenAI │
│ https://api.openai.com/v1 │
│ 模型: gpt-4o, gpt-4o-mini │
│ │
│ ○ DeepSeek │
│ https://api.deepseek.com │
│ 模型: deepseek-chat, deepseek-reasoner │
│ │
│ ○ 本地 Ollama │
│ http://127.0.0.1:11434/v1 │
│ 模型: qwen2.5:7b │
│ │
│ 选择一个服务商,自动填充配置。 │
│ [取消] [导入] │
└─────────────────────────────────────────────┘
```
### 后端
```
Tauri: 已有 read_openclaw_config 命令
dev-api.js: 已有 read_config handler
// 只需在前端加一个读取+解析+填充的逻辑
```
### 优先级:🔴 高(零成本,纯前端,极大提升体验)
### 工时估算0.5 天
---
## 实施路线图
### Phase 1快速见效1-2 天)
| 序号 | 功能 | 工时 | 理由 |
|------|------|------|------|
| 1 | **模型配置自动导入** | 0.5d | 读 Agent models.json → 一键填充,纯前端零风险 |
| 2 | **联网搜索工具** | 0.5-1d | DuckDuckGo + Jina免费无 Key |
| 3 | **灵魂移植(借尸还魂)** | 1-2d | 杀手级差异化——读 SOUL/IDENTITY/USER/AGENTS/MEMORY → 变身 |
### Phase 2核心扩展2-3 天)
| 序号 | 功能 | 工时 | 理由 |
|------|------|------|------|
| 4 | **Docker/WSL 工具** | 1-2d | 解决用户最常见的安装困惑 |
| 5 | **自定义知识库 V1** | 1d | 用户上传 md/txt → 注入 prompt |
### Phase 3高级功能3-5 天)
| 序号 | 功能 | 工时 | 理由 |
|------|------|------|------|
| 6 | **SSH 远程管理** | 2-3d | 价值最高但复杂度也最高 |
| 7 | **知识库 V2语义搜索** | 3-5d | 依赖 embedding API |
---
## 设置面板 Tab 规划
当前 3 个 Tab → 扩展为 5 个 Tab
```
模型配置 │ 工具权限 │ 知识库 │ 远程连接 │ 助手人设
```
### 工具权限 Tab 最终形态
```
基础工具
☑ 终端工具 — 允许执行 Shell 命令
☑ 文件工具 — 允许读写文件和浏览目录
扩展工具
☐ Docker/WSL — 允许管理容器和 WSL 环境
☐ 联网搜索 — 允许搜索互联网和抓取网页
☐ SSH 远程 — 允许连接远程服务器(需先配置连接)
☐ 知识库 — 允许检索知识库内容
进程列表、端口检测、系统信息工具始终可用(非聊天模式下)。
```
---
## 技术注意事项
### 1. Token 预算管理
灵魂移植 + 知识库注入会占用 context window需要精细管理
| 组件 | 预算 | 说明 |
|------|------|------|
| ClawPanel 基础 prompt | ~2000 tokens | 产品介绍、工具指南、技能卡片 |
| SOUL.md | ~500 tokens | 人设通常简短 |
| IDENTITY.md | ~200 tokens | 名称/风格 |
| USER.md | ~200 tokens | 用户档案 |
| AGENTS.md | ~3000 tokens | 操作规则(最大,可截断) |
| TOOLS.md | ~300 tokens | 工具笔记 |
| MEMORY.md | ~2000 tokens | 长期记忆(截断保留最近部分) |
| 每日记忆 (3天) | ~1500 tokens | 自动截断 |
| 自定义知识库 | ~4000 tokens | 用户上传文档 |
| 搜索结果 | ~2000 tokens | web_search 返回内容 |
| **总计上限** | **~16000 tokens** | 留足空间给对话历史 |
策略:
- AGENTS.md 超过 3000 tokens 时截断尾部,保留前面的核心规则
- MEMORY.md 超过 2000 tokens 时只保留最后 2000 tokens
- 每日记忆超过 500 tokens/天时截断
- 灵魂文件加载时计算总 token 并在 UI 中显示
### 2. 跨平台兼容
- Docker CLI 在 Windows/Mac/Linux 都可用
- WSL 仅 Windows
- SSH 密钥路径Windows 用 `%USERPROFILE%\.ssh\`Mac/Linux 用 `~/.ssh/`
### 3. 安全存储
- SSH 密码/API Key
- Tauri 模式:使用 keytar 或 tauri-plugin-store 加密存储
- Web 模式:仅支持密钥认证(不存储密码)
- 知识库文件:存储在 `~/.openclaw/` 下,与用户数据同目录
### 4. 工具发现
AI 模型需要知道哪些工具可用。当前已在 `buildSystemPrompt()` 中列出技能卡片。
新增工具后,需要在系统提示词中补充使用指南(类似现有的 `ask_user` 指南)。
---
## 文件变更预估
| 文件 | 变更 |
|------|------|
| `src/pages/assistant.js` | TOOL_DEFS 新增 4 类 · executeTool 新增 case · getEnabledTools 新增分支 · 设置面板 UI · 模型导入弹窗 |
| `src-tauri/src/commands/assistant.rs` | 新增 Rust 命令docker_*, wsl_*, ssh_*, web_search, fetch_url |
| `scripts/dev-api.js` | 新增 Web 模式 handler同上 |
| `src/style/assistant.css` | 知识库管理 UI · SSH 连接管理 UI · 导入弹窗样式 |
| `src/pages/assistant.js` (prompt) | 系统提示词新增各工具使用指南 |

View File

@@ -18,7 +18,7 @@
<meta property="og:image" content="https://claw.qt.cool/00.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="675">
<meta property="og:image:alt" content="ClawPanel AI 助手截图">
<meta property="og:image:alt" content="ClawPanel 仪表盘">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="ClawPanel - OpenClaw AI Agent 可视化管理面板">
<meta name="twitter:description" content="基于 Tauri v2 的跨平台桌面应用。多模型配置、实时 AI 聊天、Agent 管理、内网穿透,一站式管理你的 AI 智能体。">
@@ -34,7 +34,7 @@
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。",
"url": "https://claw.qt.cool/",
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
"softwareVersion": "0.5.7",
"softwareVersion": "0.6.0",
"author": {
"@type": "Organization",
"name": "晴辰云 QingchenCloud",
@@ -206,13 +206,13 @@
.screenshot-frame img { width: 100%; display: block; }
/* ══════════════ Gallery ══════════════ */
.gallery-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 16px; margin-bottom: 24px; }
.gallery-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; margin-bottom: 24px; }
.gallery-card { border-radius: 12px; overflow: hidden; border: 1px solid var(--border); box-shadow: var(--card-shadow); transition: transform 0.4s cubic-bezier(0.16,1,0.3,1), box-shadow 0.4s, border-color 0.4s; cursor: zoom-in; position: relative; }
.gallery-card:hover { transform: translateY(-4px) scale(1.02); box-shadow: 0 12px 40px -8px rgba(99,102,241,0.2); border-color: var(--border-h); }
.gallery-card img { width: 100%; display: block; }
.gallery-label { position: absolute; bottom: 0; left: 0; right: 0; padding: 8px 12px; background: linear-gradient(transparent, rgba(0,0,0,0.65)); color: #fff; font-size: 13px; font-weight: 600; opacity: 0; transition: opacity 0.3s; }
.gallery-card:hover .gallery-label { opacity: 1; }
.info-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 16px; }
.info-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
.info-card { border-radius: 16px; border: 1px solid var(--border); background: var(--bg-card); backdrop-filter: blur(8px); padding: 20px; }
.info-card .ic { font-size: 18px; margin-bottom: 8px; }
.info-card h4 { font-size: 14px; font-weight: 700; margin-bottom: 4px; }
@@ -452,6 +452,7 @@
<a href="#deploy" class="nav-link">部署</a>
<a href="#docs" class="nav-link">文档</a>
<a href="#community" class="nav-link">社区</a>
<a href="#ai-plan" class="nav-link">活动</a>
<a href="#download" class="nav-link">下载</a>
<a href="https://github.com/qingchencloud/clawpanel" target="_blank" rel="noopener" class="nav-link">GitHub</a>
<button id="theme-toggle" class="theme-btn" aria-label="切换主题">
@@ -477,6 +478,7 @@
<a href="#deploy" class="mobile-menu-link mobile-link">部署</a>
<a href="#docs" class="mobile-menu-link mobile-link">文档</a>
<a href="#community" class="mobile-menu-link mobile-link">社区</a>
<a href="#ai-plan" class="mobile-menu-link mobile-link">活动</a>
<a href="https://github.com/qingchencloud/clawpanel" target="_blank" rel="noopener" class="mobile-menu-link">GitHub ↗</a>
</div>
</div>
@@ -506,7 +508,7 @@
</a>
</div>
<div class="reveal hero-image-wrap" data-delay="300">
<img src="./00.png" alt="ClawPanel AI 助手" onclick="openLightbox(this.src)" loading="eager">
<img src="./01.png" alt="ClawPanel AI 助手 — 8 大技能卡片" onclick="openLightbox(this.src)" loading="eager">
<div class="hero-glow"></div>
</div>
</div>
@@ -562,149 +564,132 @@
<p class="reveal section-desc">一个面板,管理 OpenClaw 的方方面面</p>
</div>
<!-- 🔥 AI 助手(工具调用) -->
<!-- 仪表盘 -->
<div class="showcase-row">
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./00.png')"><img src="./00.png" alt="AI 助手 — 8 大技能卡片" loading="lazy"></div>
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./00.png')"><img src="./00.png" alt="ClawPanel 仪表盘" loading="lazy"></div>
<div>
<div class="reveal showcase-tag" style="color:#f43f5e;background:rgba(244,63,94,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/><path d="M18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"/></svg> 🔥 核心亮点</div>
<h3 class="reveal showcase-title">内置 AI 助手 — 帮你管理 OpenClaw</h3>
<p class="reveal showcase-desc">不只是聊天——AI 助手能<strong>一键安装 OpenClaw</strong>、自动读取配置、浏览目录、执行命令帮你诊断问题、修复错误。8 大技能卡片一键触发,新手也能轻松管理</p>
<div class="reveal showcase-tag" style="color:#f43f5e;background:rgba(244,63,94,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> 全景概览</div>
<h3 class="reveal showcase-title">仪表盘 — 运行状态一目了然</h3>
<p class="reveal showcase-desc">Gateway 运行状态、版本信息、Agent 数量、模型池总览,搭配内网穿透状态、基础服务监控和实时日志流。一屏掌握 OpenClaw 所有运行指标</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 一键安装 &amp; 升级 OpenClaw</li>
<li><span class="check"></span> 终端执行 &amp; 文件读写 &amp; 目录浏览</li>
<li><span class="check"></span> 4 种模式(聊天/规划/执行/无限)</li>
<li><span class="check"></span> 危险操作二次确认,安全可控</li>
<li><span class="check"></span> 独立模型配置,兼容任意 OpenAI 格式 API</li>
<li><span class="check"></span> Gateway / 隧道 / 服务实时状态卡片</li>
<li><span class="check"></span> 配置版本标识 &amp; 最近备份时间</li>
<li><span class="check"></span> 并行推理队列 &amp; 工作区文件隔离</li>
<li><span class="check"></span> 重启 / 检查更新 / 创建备份快捷操作</li>
</ul>
</div>
</div>
<!-- AI 助手实战:工具调用 -->
<!-- AI 助手设置 — 模型配置 + 公益 AI 接口 -->
<div class="showcase-row">
<div>
<div class="reveal showcase-tag" style="color:#f43f5e;background:rgba(244,63,94,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg> AI 实战</div>
<h3 class="reveal showcase-title">AI 自动调用工具诊断问题</h3>
<p class="reveal showcase-desc">点击「检查配置」技能卡片AI 自动执行:获取系统信息 → 列出目录 → 读取 openclaw.json → 读取 models.json → 生成健康检查报告。全程可视化,每步工具调用的参数和结果一目了然</p>
<div class="reveal showcase-tag" style="color:#f43f5e;background:rgba(244,63,94,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/><path d="M18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"/></svg> 开箱即用</div>
<h3 class="reveal showcase-title">公益 AI 接口 — 零成本开始</h3>
<p class="reveal showcase-desc">内置<strong>公益 AI 接口计划</strong>GPT-5 全系列模型免费使用Token 费用由项目组内部承担。选择模型、一键接入,无需注册、无需付费。也支持接入任意 OpenAI 兼容 API</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 实时工具调用可视化(参数 + 结果 + 状态)</li>
<li><span class="check"></span> AI 自动编排多步骤诊断流程</li>
<li><span class="check"></span> 生成结构化健康检查报告</li>
<li><span class="check"></span> 发现问题自动给出修复建议</li>
<li><span class="check"></span> 公益接口一键接入,零门槛</li>
<li><span class="check"></span> GPT-5 全系列模型可选</li>
<li><span class="check"></span> 兼容 Chat Completions &amp; Responses API</li>
<li><span class="check"></span> 独立配置,无需安装 OpenClaw</li>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./11.png')"><img src="./11.png" alt="AI 助手工具调用实战" loading="lazy"></div>
<div class="reveal screenshot-frame" onclick="openLightbox('./02.png')"><img src="./02.png" alt="AI 助手设置 — 公益 AI 接口一键接入" loading="lazy"></div>
</div>
<!-- AI 助手设置 -->
<!-- AI 助手人设 — Agent 灵魂 -->
<div class="showcase-row">
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./12.png')"><img src="./12.png" alt="AI 助手设置" loading="lazy"></div>
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./05.png')"><img src="./05.png" alt="AI 助手人设 — Agent 灵魂加载" loading="lazy"></div>
<div>
<div class="reveal showcase-tag" style="color:#f43f5e;background:rgba(244,63,94,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg> 灵活配置</div>
<h3 class="reveal showcase-title">独立模型配置,开箱即用</h3>
<p class="reveal showcase-desc">AI 助手使用独立的模型配置,不依赖 OpenClaw Gateway。填入任意兼容 OpenAI 格式的 APIDeepSeek、Kimi、通义千问等即可开始使用。自动兼容 Chat Completions 和 Responses 两种 API 格式</p>
<div class="reveal showcase-tag" style="color:#f43f5e;background:rgba(244,63,94,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> 借尸还魂</div>
<h3 class="reveal showcase-title">继承 Agent 人格与记忆</h3>
<p class="reveal showcase-desc">从 OpenClaw Agent 加载完整灵魂——SOUL.md人格、IDENTITY.md身份、USER.md用户偏好、AGENTS.md行为规则、TOOLS.md工具一键注入。让 AI 助手拥有你 Agent 的全部能力</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 支持所有 OpenAI 兼容 API</li>
<li><span class="check"></span> 模型配置、工具权限、助手人设三合一</li>
<li><span class="check"></span> 一键测试连通性 &amp; 自动拉取模型列表</li>
<li><span class="check"></span> 无需安装 OpenClaw 也能使用 AI 助手</li>
<li><span class="check"></span> 一键加载 Agent 灵魂5/6 核心文件)</li>
<li><span class="check"></span> 多 Agent 灵魂自由切换</li>
<li><span class="check"></span> 保留 ClawPanel 工具调用能力</li>
<li><span class="check"></span> 工具权限细粒度控制</li>
</ul>
</div>
</div>
<!-- AI 图片识别 -->
<div class="showcase-row">
<div>
<div class="reveal showcase-tag" style="color:#f43f5e;background:rgba(244,63,94,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg> 多模态</div>
<h3 class="reveal showcase-title">图片识别 — AI 一眼看懂</h3>
<p class="reveal showcase-desc">直接粘贴截图或拖拽图片到对话框AI 自动识别图片内容并给出详细分析。支持截图、照片、文档图片等多种格式,真正的多模态交互体验。</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> Ctrl+V 粘贴截图直接发送</li>
<li><span class="check"></span> 拖拽图片文件到对话框</li>
<li><span class="check"></span> AI 自动识别并分析图片内容</li>
<li><span class="check"></span> 图文混排 · 多模态对话</li>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./13.png')"><img src="./13.png" alt="AI 图片识别" loading="lazy"></div>
</div>
<!-- 实时聊天 -->
<div class="showcase-row">
<div>
<div class="reveal showcase-tag c-indigo" style="background:var(--accent-10)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> 核心功能</div>
<h3 class="reveal showcase-title">实时 AI 对话</h3>
<p class="reveal showcase-desc">WebSocket 直连 Gateway流式响应实时显示。支持图片附件、多模态交互、Markdown 渲染、会话管理与快捷指令。</p>
<h3 class="reveal showcase-title">实时聊天 — 多模型流式对话</h3>
<p class="reveal showcase-desc">WebSocket 直连 Gateway流式响应逐字显示。自动列出所有已配置模型(含公益接口),支持图片附件、Markdown 渲染快捷指令。</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 流式传输,逐字显示</li>
<li><span class="check"></span> 多 Provider 模型自动聚合</li>
<li><span class="check"></span> 多会话管理与历史记录</li>
<li><span class="check"></span> 图片拖拽上传 &amp; 灯箱预览</li>
<li><span class="check"></span> 图片拖拽 &amp; 多模态对话</li>
<li><span class="check"></span> / 快捷指令系统</li>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./02.png')"><img src="./02.png" alt="实时聊天" loading="lazy"></div>
<div class="reveal screenshot-frame" onclick="openLightbox('./07.png')"><img src="./07.png" alt="实时聊天" loading="lazy"></div>
</div>
<!-- 模型配置 -->
<div class="showcase-row">
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./09.png')"><img src="./09.png" alt="模型配置" loading="lazy"></div>
<div>
<div class="reveal showcase-tag" style="color:#a855f7;background:rgba(168,85,247,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg> 配置中心</div>
<h3 class="reveal showcase-title">模型灵活调配</h3>
<p class="reveal showcase-desc">支持 OpenAI、DeepSeek、Kimi 等多家服务商。可视化管理模型列表,一键设为主模型,自动备选切换</p>
<h3 class="reveal showcase-title">服务商统一管理</h3>
<p class="reveal showcase-desc">OpenAI、公益接口、DeepSeek、Kimi 等多家服务商统一管理。内置公益 AI 接口一键添加全部模型。可视化主模型 + 备选自动切换,批量测试连通性</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 多 Provider 统一管理</li>
<li><span class="check"></span> 批量连通性测试</li>
<li><span class="check"></span> 公益接口一键添加全部模型</li>
<li><span class="check"></span> 批量连通性测试 &amp; 延迟检测</li>
<li><span class="check"></span> 主模型 + 备选自动切换</li>
<li><span class="check"></span> 拖拽排序 &amp; 实时保存</li>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./05.png')"><img src="./05.png" alt="模型配置" loading="lazy"></div>
</div>
<!-- 记忆文件 -->
<div class="showcase-row">
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./08.png')"><img src="./08.png" alt="记忆文件" loading="lazy"></div>
<div>
<div class="reveal showcase-tag" style="color:#22d3ee;background:rgba(34,211,238,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg> 数据管理</div>
<h3 class="reveal showcase-title">让 Agent 拥有记忆</h3>
<p class="reveal showcase-desc">在线编辑 Agent 核心配置文件AGENTS.md、SOUL.md 等),管理工作记忆和记忆归档。支持 ZIP 一键打包导出</p>
<p class="reveal showcase-desc">在线编辑 Agent 工作记忆、浏览记忆归档、管理核心配置文件SOUL.md、AGENTS.md 等)。支持 ZIP 一键打包导出,多 Agent 记忆完全隔离</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 核心文件在线编辑</li>
<li><span class="check"></span> 工作记忆 &amp; 记忆归档</li>
<li><span class="check"></span> 工作记忆 &amp; 记忆归档 &amp; 核心文件</li>
<li><span class="check"></span> 在线编辑 &amp; 实时预览</li>
<li><span class="check"></span> 多 Agent 记忆隔离</li>
<li><span class="check"></span> ZIP 打包下载</li>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./13.png')"><img src="./13.png" alt="记忆文件" loading="lazy"></div>
</div>
<!-- Gateway -->
<!-- Gateway 安全认证 -->
<div class="showcase-row">
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./11.png')"><img src="./11.png" alt="Gateway 安全认证与工具权限" loading="lazy"></div>
<div>
<div class="reveal showcase-tag" style="color:#10b981;background:rgba(16,185,129,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg> 网关管控</div>
<h3 class="reveal showcase-title">安全访问,可视化配置</h3>
<p class="reveal showcase-desc">Gateway 端口、绑定范围、运行模式、Token / 密码认证式,卡片式选项直观配置,即改即生效</p>
<div class="reveal showcase-tag" style="color:#10b981;background:rgba(16,185,129,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> 安全防护</div>
<h3 class="reveal showcase-title">安全认证 + 工具权限管控</h3>
<p class="reveal showcase-desc">Token 密钥 / 密码认证双模式,卡片式直观选择。Agent 工具调用权限三档可调(完整 / 受限 / 禁用),会话可见性细粒度控制。安全与灵活兼得</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 本地 / 局域网模式切换</li>
<li><span class="check"></span> Token &amp; 密码双认证模式</li>
<li><span class="check"></span> Tailscale Funnel 支持</li>
<li><span class="check"></span> 高级选项按需展开</li>
<li><span class="check"></span> Agent 工具权限三档管控</li>
<li><span class="check"></span> 会话可见性控制</li>
<li><span class="check"></span> 改完自动重启生效</li>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./07.png')"><img src="./07.png" alt="Gateway 配置" loading="lazy"></div>
</div>
<!-- Agent 管理 -->
<div class="showcase-row">
<div class="reveal screenshot-frame img-first" onclick="openLightbox('./06.png')"><img src="./06.png" alt="Agent 管理" loading="lazy"></div>
<div>
<div class="reveal showcase-tag" style="color:#f97316;background:rgba(249,115,22,0.1)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="10" x="3" y="11" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4"/><line x1="8" x2="8" y1="16" y2="16"/><line x1="16" x2="16" y1="16" y2="16"/></svg> 智能体</div>
<h3 class="reveal showcase-title">多 Agent 协作管理</h3>
<p class="reveal showcase-desc">创建和管理多个 AI Agent配置各自的身份、模型和独立工作区。支持备份、编辑与一键切换。</p>
<p class="reveal showcase-desc">创建和管理多个 AI Agent各自配置名称、模型和独立工作区。支持备份、编辑与默认 Agent 快速切换。</p>
<ul class="reveal showcase-list">
<li><span class="check"></span> 多 Agent 独立工作区</li>
<li><span class="check"></span> 身份与模型单独配置</li>
<li><span class="check"></span> Agent 配置备份</li>
<li><span class="check"></span> Agent 配置备份 &amp; 编辑</li>
<li><span class="check"></span> 默认 Agent 快速切换</li>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./10.png')"><img src="./10.png" alt="Agent 管理" loading="lazy"></div>
</div>
</div>
</section>
@@ -719,15 +704,19 @@
</div>
<div class="gallery-grid">
<div class="reveal gallery-card" onclick="openLightbox('./03.png')"><img src="./03.png" alt="服务管理" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg> 服务管理</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./04.png')"><img src="./04.png" alt="日志查看" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M15 2H9a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1z"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg> 日志查看</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./09.png')"><img src="./09.png" alt="扩展工具" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg> 扩展工具</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./10.png')"><img src="./10.png" alt="系统诊断" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> 系统诊断</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./06.png')"><img src="./06.png" alt="知识库" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg> 知识库</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./12.png')"><img src="./12.png" alt="安全设置" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> 安全设置</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./14.png')"><img src="./14.png" alt="扩展工具" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg> 扩展工具</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./15.png')"><img src="./15.png" alt="系统诊断" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg> 系统诊断</div></div>
<div class="reveal gallery-card" onclick="openLightbox('./16.png')"><img src="./16.png" alt="关于" loading="lazy"><div class="gallery-label"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-2px"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg> 关于</div></div>
</div>
<div class="info-grid">
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div><h4>服务管理</h4><p>启停控制、版本检测、一键升级、配置备份与恢复</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M15 2H9a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1z"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg></div><h4>日志查看</h4><p>多日志源实时查看、关键字搜索、自动滚动跟踪</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg></div><h4>扩展工具</h4><p>cftunnel 内网穿透、ClawApp 移动客户端管理</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></div><h4>系统诊断</h4><p>全面健康检测、WebSocket 测试、一键修复配对</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></div><h4>服务管理</h4><p>启停控制、版本检测、一键升级、npm 源切换、配置备份与恢复</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/></svg></div><h4>知识库</h4><p>自定义知识注入 AI 助手Markdown 格式,对话时自动激活</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div><h4>安全设置</h4><p>访问密码保护、无视风险模式切换、面板访问安全管控</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22v-5"/><path d="M9 8V2"/><path d="M15 8V2"/><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"/></svg></div><h4>扩展工具</h4><p>cftunnel 内网穿透、ClawApp 移动客户端一键安装</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></div><h4>系统诊断</h4><p>全面健康检测、WebSocket 测试、网络日志、一键修复配对</p></div>
<div class="reveal info-card"><div class="ic"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></div><h4>关于</h4><p>版本信息、社群入口QQ / 微信 / 抖音)、相关项目链接</p></div>
</div>
</div>
</section>
@@ -983,7 +972,7 @@
<div class="orb orb-2" style="top:auto;bottom:-100px"></div>
<div class="container-sm" style="position:relative;z-index:10">
<div class="section-header">
<div class="reveal download-version"><span class="pulse"></span> v0.5.7 最新版</div>
<div class="reveal download-version"><span class="pulse"></span> v0.6.0 最新版</div>
<h2 class="reveal section-title"><span class="gradient-text">下载安装</span></h2>
<p class="reveal section-desc">选择你的操作系统,一键下载安装</p>
</div>
@@ -993,11 +982,11 @@
<h3>macOS</h3>
<p class="dl-desc">支持 Apple Silicon 和 Intel 芯片</p>
<div class="dl-links">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.5.7_aarch64.dmg" target="_blank" rel="noopener">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.6.0_aarch64.dmg" target="_blank" rel="noopener">
Apple Silicon (M1/M2/M3/M4)
<span class="dl-format">.dmg</span>
</a>
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.5.7_x64.dmg" target="_blank" rel="noopener">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.6.0_x64.dmg" target="_blank" rel="noopener">
Intel 芯片
<span class="dl-format">.dmg</span>
</a>
@@ -1008,11 +997,11 @@
<h3>Windows</h3>
<p class="dl-desc">支持 Windows 10 及以上版本</p>
<div class="dl-links">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.5.7_x64-setup.exe" target="_blank" rel="noopener">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.6.0_x64-setup.exe" target="_blank" rel="noopener">
安装程序
<span class="dl-format">.exe</span>
</a>
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.5.7_x64_en-US.msi" target="_blank" rel="noopener">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.6.0_x64_en-US.msi" target="_blank" rel="noopener">
MSI 安装包
<span class="dl-format">.msi</span>
</a>
@@ -1023,11 +1012,11 @@
<h3>Linux</h3>
<p class="dl-desc">支持主流 Linux 发行版</p>
<div class="dl-links">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.5.7_amd64.AppImage" target="_blank" rel="noopener">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.6.0_amd64.AppImage" target="_blank" rel="noopener">
通用版
<span class="dl-format">.AppImage</span>
</a>
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.5.7_amd64.deb" target="_blank" rel="noopener">
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.6.0_amd64.deb" target="_blank" rel="noopener">
Debian / Ubuntu
<span class="dl-format">.deb</span>
</a>
@@ -1049,7 +1038,7 @@
<h2 class="reveal section-title"><span class="gradient-text">社区交流</span></h2>
<p class="reveal section-desc">加入社区,交流使用心得、反馈问题、获取最新动态</p>
</div>
<div class="community-grid">
<div class="community-grid" style="grid-template-columns:repeat(3,1fr)">
<div class="reveal community-card">
<img class="community-qr" src="./qr-qq.png" alt="QQ 群二维码">
<h3>QQ 交流群</h3>
@@ -1062,6 +1051,12 @@
<p class="desc">加入微信群,获取最新版本通知和使用技巧分享</p>
<a href="https://qt.cool/c/OpenClawWx" target="_blank" rel="noopener" class="btn btn-sm btn-outline">加入微信群</a>
</div>
<div class="reveal community-card">
<img class="community-qr" src="https://qt.cool/c/OpenClawDY/qr.png" alt="抖音群二维码" style="object-fit:contain;background:#fff">
<h3>抖音交流群</h3>
<p class="desc">关注抖音账号,观看教程视频和最新动态</p>
<a href="https://qt.cool/c/OpenClawDY" target="_blank" rel="noopener" class="btn btn-sm btn-outline">加入抖音群</a>
</div>
</div>
<div class="community-extra">
<a href="https://discord.gg/U9AttmsNHh" target="_blank" rel="noopener" class="reveal community-link">
@@ -1084,6 +1079,50 @@
</div>
</section>
<!-- ══════════════ 公益 AI 接口计划 ══════════════ -->
<section id="ai-plan" class="section" style="padding-top:64px;padding-bottom:64px">
<div class="grid-bg"></div>
<div class="container" style="position:relative;z-index:10">
<div class="section-header">
<h2 class="reveal section-title"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:inline;vertical-align:-6px;margin-right:4px;color:var(--accent)"><path d="M20 12v10H4V12"/><path d="M2 7h20v5H2z"/><path d="M12 22V7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg> <span class="gradient-text">公益 AI 接口计划</span></h2>
<p class="reveal section-desc">Token 费用?我们帮你出了。</p>
</div>
<div class="reveal" style="max-width:800px;margin:0 auto;border-radius:20px;background:linear-gradient(135deg,#0f0c29 0%,#302b63 50%,#24243e 100%);color:#fff;position:relative;overflow:hidden;box-shadow:0 8px 40px rgba(48,43,99,0.3)">
<div style="position:absolute;top:-60px;right:-60px;width:200px;height:200px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,0.15) 0%,transparent 70%);pointer-events:none"></div>
<div style="position:absolute;bottom:-40px;left:-40px;width:150px;height:150px;border-radius:50%;background:radial-gradient(circle,rgba(168,85,247,0.1) 0%,transparent 70%);pointer-events:none"></div>
<div style="padding:40px 48px;position:relative">
<div style="font-size:15px;color:rgba(255,255,255,0.8);line-height:1.9;margin-bottom:24px">
<p style="margin-bottom:12px">ClawPanel 公益 AI 接口测试计划已开放。<strong style="color:#a78bfa">调用成本由项目组内部承担</strong>,用户无需注册、无需付费。</p>
<p style="margin-bottom:12px">支持 GPT-5 全系列模型,兼容 OpenAI <code style="background:rgba(255,255,255,0.1);padding:2px 6px;border-radius:4px;font-size:13px">/v1/chat/completions</code><code style="background:rgba(255,255,255,0.1);padding:2px 6px;border-radius:4px;font-size:13px">/v1/responses</code> 接口。</p>
<p>在 ClawPanel 内置助手设置中,选择模型 → 点击「测试」→「一键接入」即可开始使用。如需独立密钥,可前往活动站签到领取。</p>
</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px">
<div style="background:rgba(255,255,255,0.06);border-radius:12px;padding:16px;text-align:center;border:1px solid rgba(255,255,255,0.08)">
<div style="margin-bottom:6px"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#a78bfa" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></div>
<div style="font-weight:700;font-size:14px;margin-bottom:4px">GPT-5 全系列</div>
<div style="font-size:12px;color:rgba(255,255,255,0.5)">10+ 模型开箱即用</div>
</div>
<div style="background:rgba(255,255,255,0.06);border-radius:12px;padding:16px;text-align:center;border:1px solid rgba(255,255,255,0.08)">
<div style="margin-bottom:6px"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#a78bfa" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"/><path d="M12 18V6"/></svg></div>
<div style="font-weight:700;font-size:14px;margin-bottom:4px">费用全包</div>
<div style="font-size:12px;color:rgba(255,255,255,0.5)">Token 成本项目组承担</div>
</div>
<div style="background:rgba(255,255,255,0.06);border-radius:12px;padding:16px;text-align:center;border:1px solid rgba(255,255,255,0.08)">
<div style="margin-bottom:6px"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#a78bfa" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m21 2-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"/></svg></div>
<div style="font-weight:700;font-size:14px;margin-bottom:4px">独立密钥</div>
<div style="font-size:12px;color:rgba(255,255,255,0.5)">签到即可免费领取</div>
</div>
</div>
<div style="display:flex;flex-wrap:wrap;gap:12px;justify-content:center">
<a href="https://gpt.qt.cool/checkin" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:8px;padding:10px 24px;border-radius:12px;background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;font-weight:600;font-size:14px;text-decoration:none;box-shadow:0 4px 16px rgba(99,102,241,0.4);transition:transform 0.2s"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 12l2 2 4-4"/></svg> 签到领密钥</a>
<a href="https://gpt.qt.cool/user" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:8px;padding:10px 24px;border-radius:12px;border:1px solid rgba(255,255,255,0.2);color:#fff;font-size:14px;font-weight:500;text-decoration:none;transition:all 0.2s"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/></svg> 用量查询</a>
<a href="https://gpt.qt.cool/" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:8px;padding:10px 24px;border-radius:12px;border:1px solid rgba(255,255,255,0.2);color:#fff;font-size:14px;font-weight:500;text-decoration:none;transition:all 0.2s"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg> GPT-AI 网关</a>
</div>
</div>
</div>
</div>
</section>
<!-- ══════════════ Sponsors ══════════════ -->
<section class="section" id="sponsors">
<div class="container">
@@ -1107,7 +1146,7 @@
<div class="footer-inner">
<div class="footer-logo">
<img src="./logo.png" alt="Logo">
<span>ClawPanel &copy; 2025</span>
<span>ClawPanel &copy; 2026</span>
</div>
<div class="footer-links">
<a href="https://github.com/qingchencloud/clawpanel" class="footer-link">GitHub</a>

5
package-lock.json generated
View File

@@ -1,12 +1,13 @@
{
"name": "clawpanel",
"version": "0.5.2",
"version": "0.6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawpanel",
"version": "0.5.2",
"version": "0.6.0",
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-shell": "^2.2.1"

View File

@@ -1,6 +1,6 @@
{
"name": "clawpanel",
"version": "0.5.7",
"version": "0.6.0",
"private": true,
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
"type": "module",

View File

@@ -5,8 +5,10 @@
*/
import fs from 'fs'
import path from 'path'
import os from 'os'
import { homedir, networkInterfaces } from 'os'
import { execSync, spawn } from 'child_process'
import net from 'net'
import crypto from 'crypto'
const OPENCLAW_DIR = path.join(homedir(), '.openclaw')
@@ -219,13 +221,14 @@ function patchGatewayOrigins() {
for (const ip of getLocalIps()) {
origins.push(`http://${ip}:1420`)
}
const newOrigins = [...new Set(origins)]
const existing = config?.gateway?.controlUi?.allowedOrigins || []
// 合并:保留用户已有的 origins只追加 ClawPanel 需要的
const merged = [...new Set([...existing, ...origins])]
// 幂等:已包含所有需要的 origin 时跳过写入
if (newOrigins.every(o => existing.includes(o))) return false
if (origins.every(o => existing.includes(o))) return false
if (!config.gateway) config.gateway = {}
if (!config.gateway.controlUi) config.gateway.controlUi = {}
config.gateway.controlUi.allowedOrigins = newOrigins
config.gateway.controlUi.allowedOrigins = merged
fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.bak')
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
return true
@@ -305,47 +308,50 @@ function winStartGateway() {
const timestamp = new Date().toISOString()
fs.appendFileSync(logPath, `\n[${timestamp}] [ClawPanel] Starting Gateway on Windows...\n`)
const child = spawn('openclaw', ['gateway'], {
// 用 cmd.exe /c 启动,不用 shell: true避免额外 cmd.exe 进程链导致终端闪烁)
const child = spawn('cmd.exe', ['/c', 'openclaw', 'gateway'], {
detached: true,
stdio: ['ignore', out, err],
shell: true,
windowsHide: true,
cwd: homedir(),
})
child.unref()
}
function winStopGateway() {
const { running, pid } = winCheckGateway()
if (!running || !pid) throw new Error('Gateway 未运行')
async function winStopGateway() {
const { running } = await winCheckGateway()
if (!running) throw new Error('Gateway 未运行')
try {
execSync(`taskkill /F /PID ${pid} /T`, { timeout: 5000, windowsHide: true })
} catch (e) {
throw new Error('停止失败: ' + (e.message || e))
execSync('taskkill /F /IM node.exe /FI "WINDOWTITLE eq openclaw*"', { timeout: 5000, windowsHide: true })
} catch {
try {
execSync('taskkill /F /IM node.exe', { timeout: 5000, windowsHide: true })
} catch (e) {
throw new Error('停止失败: ' + (e.message || e))
}
}
}
// TCP 探测 Gateway 端口(纯异步,零子进程,不会闪终端)
function winCheckGateway() {
const port = readGatewayPort()
try {
// 用 netstat 精确查找监听指定端口的进程 PID
const out = execSync(`netstat -ano | findstr ":${port}" | findstr "LISTENING"`, { timeout: 3000, windowsHide: true }).toString().trim()
if (!out) return { running: false, pid: null }
// 提取 PID最后一列
const parts = out.split('\n')[0].trim().split(/\s+/)
const pid = parseInt(parts[parts.length - 1]) || null
if (!pid) return { running: false, pid: null }
// 验证进程是否为 node/openclaw排除其他程序碰巧占用同端口
try {
const taskOut = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { timeout: 3000, windowsHide: true }).toString().trim()
const isGateway = /node|openclaw/i.test(taskOut)
return { running: isGateway, pid: isGateway ? pid : null }
} catch {
return { running: true, pid }
}
} catch {
return { running: false, pid: null }
}
return new Promise(resolve => {
const sock = new net.Socket()
sock.setTimeout(300)
sock.once('connect', () => {
sock.destroy()
resolve({ running: true, pid: null })
})
sock.once('error', () => {
sock.destroy()
resolve({ running: false, pid: null })
})
sock.once('timeout', () => {
sock.destroy()
resolve({ running: false, pid: null })
})
sock.connect(port, '127.0.0.1')
})
}
function readGatewayPort() {
@@ -506,9 +512,9 @@ const handlers = {
},
// 服务管理
get_services_status() {
async get_services_status() {
const label = 'ai.openclaw.gateway'
const { running, pid } = isMac ? macCheckService(label) : isLinux ? linuxCheckGateway() : winCheckGateway()
const { running, pid } = isMac ? macCheckService(label) : isLinux ? linuxCheckGateway() : await winCheckGateway()
let cliInstalled = false
if (isMac) {
@@ -530,10 +536,10 @@ const handlers = {
return true
},
stop_service({ label }) {
async stop_service({ label }) {
if (isMac) { macStopService(label); return true }
if (isLinux) { linuxStopGateway(); return true }
winStopGateway()
await winStopGateway()
return true
},
@@ -549,9 +555,9 @@ const handlers = {
linuxStartGateway()
return true
}
try { winStopGateway() } catch {}
try { await winStopGateway() } catch {}
for (let i = 0; i < 10; i++) {
const { running } = winCheckGateway()
const { running } = await winCheckGateway()
if (!running) break
await new Promise(r => setTimeout(r, 500))
}
@@ -615,9 +621,16 @@ const handlers = {
return { current, latest: null, update_available: false, source: 'chinese' }
},
// 清理 base URL去掉尾部斜杠和已知端点路径防止路径重复
_normalizeBaseUrl(raw) {
let base = raw.replace(/\/+$/, '')
base = base.replace(/\/(chat\/completions|completions|responses|messages|models)\/?$/, '')
return base.replace(/\/+$/, '')
},
// 模型测试
async test_model({ baseUrl, apiKey, modelId }) {
const url = `${baseUrl.replace(/\/+$/, '')}/chat/completions`
const url = `${this._normalizeBaseUrl(baseUrl)}/chat/completions`
const body = JSON.stringify({
model: modelId,
messages: [{ role: 'user', content: 'Hi' }],
@@ -649,7 +662,7 @@ const handlers = {
},
async list_remote_models({ baseUrl, apiKey }) {
const url = `${baseUrl.replace(/\/+$/, '')}/models`
const url = `${this._normalizeBaseUrl(baseUrl)}/models`
const headers = {}
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
const controller = new AbortController()
@@ -829,7 +842,7 @@ const handlers = {
upgrade_openclaw({ source = 'chinese' } = {}) {
const OPENCLAW_DIR = path.join(homedir(), '.openclaw')
const pkg = source === 'official' ? '@anthropic-ai/claw' : '@qingchencloud/openclaw-zh'
const pkg = source === 'official' ? 'openclaw' : '@qingchencloud/openclaw-zh'
const npmBin = isWindows ? 'npm.cmd' : 'npm'
try {
const out = execSync(`${npmBin} install ${pkg}@latest --prefix "${OPENCLAW_DIR}" 2>&1`, { timeout: 120000, windowsHide: true }).toString()
@@ -892,46 +905,70 @@ const handlers = {
// 扩展工具
get_cftunnel_status() {
if (!isMac) return { installed: false }
// 优先使用 cftunnel CLI跨平台
const bin = isWindows ? 'cftunnel.exe' : 'cftunnel'
try {
const ver = execSync('cftunnel --version 2>&1').toString().trim()
let running = false, pid = null
try {
const pgrepOut = execSync('pgrep -f cloudflared 2>/dev/null').toString().trim()
if (pgrepOut) { running = true; pid = parseInt(pgrepOut.split('\n')[0]) || null }
} catch {}
// 读取 config.yml 获取 tunnel_name 和 routes
let tunnel_name = '', routes = []
const cfgPath = path.join(homedir(), '.cftunnel/config.yml')
if (fs.existsSync(cfgPath)) {
const cfgText = fs.readFileSync(cfgPath, 'utf8')
const nameMatch = cfgText.match(/^\s+name:\s*(.+)$/m)
if (nameMatch) tunnel_name = nameMatch[1].trim()
// 解析 routes 数组
const routeBlocks = cfgText.split(/^\s+-\s+name:/m).slice(1)
routes = routeBlocks.map(block => {
const lines = ('name:' + block).split('\n')
const get = key => { const l = lines.find(x => x.trim().startsWith(key + ':')); return l ? l.split(':').slice(1).join(':').trim() : '' }
return { name: get('name'), domain: get('hostname'), service: get('service') }
}).filter(r => r.name)
}
return { installed: true, version: ver, running, pid, tunnel_name, routes }
execSync(`${bin} --version`, { timeout: 3000, windowsHide: true, stdio: 'pipe' })
} catch {
return { installed: false }
}
// 已安装,获取状态
let running = false, pid = null, tunnel_name = ''
try {
const statusOut = execSync(`${bin} status 2>&1`, { timeout: 5000, windowsHide: true }).toString()
if (statusOut.includes('运行中')) running = true
const pidMatch = statusOut.match(/PID[:]\s*(\d+)/)
if (pidMatch) pid = parseInt(pidMatch[1])
const nameMatch = statusOut.match(/隧道[:]\s*([^\s(]+)/)
if (nameMatch) tunnel_name = nameMatch[1]
} catch {}
// 补充进程检测
if (!running) {
try {
if (isWindows) {
const out = execSync('tasklist /FI "IMAGENAME eq cftunnel.exe" /FO CSV /NH 2>nul', { timeout: 3000, windowsHide: true }).toString()
if (out.includes('cftunnel.exe')) running = true
} else {
const out = execSync('pgrep -f cftunnel 2>/dev/null', { timeout: 3000 }).toString().trim()
if (out) { running = true; pid = pid || parseInt(out.split('\n')[0]) || null }
}
} catch {}
}
// 获取路由列表
let routes = []
try {
const listOut = execSync(`${bin} list 2>&1`, { timeout: 5000, windowsHide: true }).toString()
const lines = listOut.split('\n').filter(l => l.trim() && !l.includes('---') && !l.toLowerCase().includes('name'))
routes = lines.map(l => {
const parts = l.split(/\s{2,}|\t/).map(s => s.trim()).filter(Boolean)
return parts.length >= 3 ? { name: parts[0], domain: parts[1], service: parts[2] } : null
}).filter(Boolean)
} catch {}
return { installed: true, running, pid, tunnel_name, routes }
},
get_clawapp_status() {
if (!isMac) return { installed: false, running: false, pid: null, port: 3210, url: 'http://localhost:3210' }
// 检测 ClawApp 进程是否运行Node 服务监听 3210 端口)
let running = false, pid = null, port = 3210
const port = 3210
let running = false, pid = null
// 检测端口是否在监听
try {
const lsofOut = execSync('lsof -i :3210 -t 2>/dev/null').toString().trim()
if (lsofOut) { running = true; pid = parseInt(lsofOut.split('\n')[0]) || null }
if (isWindows) {
const out = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { timeout: 3000, windowsHide: true }).toString().trim()
if (out) {
running = true
const parts = out.split(/\s+/)
pid = parseInt(parts[parts.length - 1]) || null
}
} else {
const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { timeout: 3000 }).toString().trim()
if (out) { running = true; pid = parseInt(out.split('\n')[0]) || null }
}
} catch {}
// 检测是否安装
const clawappDir = path.join(homedir(), 'Desktop/clawapp')
const installed = fs.existsSync(clawappDir)
// 检测是否安装(多个可能路径)
const candidates = isWindows
? [path.join(homedir(), 'Desktop\\clawapp'), path.join(homedir(), 'clawapp')]
: [path.join(homedir(), 'Desktop/clawapp'), path.join(homedir(), 'clawapp'), '/opt/clawapp']
const installed = candidates.some(p => fs.existsSync(p))
return { installed, running, pid, port, url: `http://localhost:${port}` }
},
@@ -1040,6 +1077,398 @@ const handlers = {
return null
},
// === AI 助手工具Web 模式真实执行) ===
assistant_exec({ command, cwd }) {
if (!command) throw new Error('命令不能为空')
// 安全限制:禁止危险命令
const dangerous = ['rm -rf /', 'mkfs', 'dd if=', ':(){', 'format ', 'del /f /s /q C:']
if (dangerous.some(d => command.includes(d))) throw new Error('危险命令已被拦截')
const opts = { timeout: 30000, maxBuffer: 1024 * 1024, windowsHide: true }
if (cwd) opts.cwd = cwd
try {
const output = execSync(command, opts).toString()
return output || '(命令已执行,无输出)'
} catch (e) {
const stderr = e.stderr?.toString() || ''
const stdout = e.stdout?.toString() || ''
return `退出码: ${e.status || 1}\n${stdout}${stderr ? '\n[stderr] ' + stderr : ''}`
}
},
assistant_read_file({ path: filePath }) {
if (!filePath) throw new Error('路径不能为空')
const expanded = filePath.startsWith('~/') ? path.join(homedir(), filePath.slice(2)) : filePath
if (!fs.existsSync(expanded)) throw new Error(`文件不存在: ${filePath}`)
const stat = fs.statSync(expanded)
if (stat.size > 1024 * 1024) throw new Error(`文件过大 (${(stat.size / 1024 / 1024).toFixed(1)}MB),最大 1MB`)
return fs.readFileSync(expanded, 'utf8')
},
assistant_write_file({ path: filePath, content }) {
if (!filePath) throw new Error('路径不能为空')
const expanded = filePath.startsWith('~/') ? path.join(homedir(), filePath.slice(2)) : filePath
const dir = path.dirname(expanded)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
fs.writeFileSync(expanded, content || '')
return `已写入 ${filePath} (${Buffer.byteLength(content || '', 'utf8')} 字节)`
},
assistant_list_dir({ path: dirPath }) {
if (!dirPath) throw new Error('路径不能为空')
const expanded = dirPath.startsWith('~/') ? path.join(homedir(), dirPath.slice(2)) : dirPath
if (!fs.existsSync(expanded)) throw new Error(`目录不存在: ${dirPath}`)
const entries = fs.readdirSync(expanded, { withFileTypes: true })
return entries.map(e => {
if (e.isDirectory()) return `[DIR] ${e.name}/`
try {
const stat = fs.statSync(path.join(expanded, e.name))
const size = stat.size < 1024 ? `${stat.size} B` : stat.size < 1048576 ? `${(stat.size / 1024).toFixed(1)} KB` : `${(stat.size / 1048576).toFixed(1)} MB`
return `[FILE] ${e.name} (${size})`
} catch {
return `[FILE] ${e.name}`
}
}).join('\n') || '(空目录)'
},
assistant_system_info() {
const platform = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux'
const arch = process.arch
const home = homedir()
const hostname = os.hostname()
const shell = process.platform === 'win32' ? 'powershell / cmd' : (process.env.SHELL || '/bin/bash')
const sep = path.sep
const totalMem = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1)
const freeMem = (os.freemem() / 1024 / 1024 / 1024).toFixed(1)
const cpus = os.cpus()
const cpuModel = cpus[0]?.model || '未知'
const lines = [
`OS: ${platform}`,
`Arch: ${arch}`,
`Home: ${home}`,
`Hostname: ${hostname}`,
`Shell: ${shell}`,
`Path separator: ${sep}`,
`CPU: ${cpuModel} (${cpus.length} 核)`,
`Memory: ${freeMem}GB free / ${totalMem}GB total`,
]
// Node.js 版本
try {
const nodeVer = execSync('node --version 2>&1', { windowsHide: true }).toString().trim()
lines.push(`Node.js: ${nodeVer}`)
} catch {}
return lines.join('\n')
},
assistant_list_processes({ filter }) {
try {
if (isWindows) {
const cmd = filter
? `tasklist /FI "IMAGENAME eq ${filter}*" /FO CSV /NH 2>nul`
: 'tasklist /FO CSV /NH 2>nul | more +1'
const output = execSync(cmd, { timeout: 5000, windowsHide: true }).toString().trim()
return output || '(无匹配进程)'
} else {
const cmd = filter
? `ps aux | head -1 && ps aux | grep -i "${filter}" | grep -v grep`
: 'ps aux | head -20'
const output = execSync(cmd, { timeout: 5000 }).toString().trim()
return output || '(无匹配进程)'
}
} catch (e) {
return e.stdout?.toString() || '(无匹配进程)'
}
},
assistant_check_port({ port }) {
if (!port) throw new Error('端口号不能为空')
try {
if (isWindows) {
const output = execSync(`netstat -ano | findstr :${port}`, { timeout: 5000, windowsHide: true }).toString().trim()
return output ? `端口 ${port} 已被占用(正在监听)\n${output}` : `端口 ${port} 未被占用(空闲)`
} else {
const output = execSync(`ss -tlnp 'sport = :${port}' 2>/dev/null || lsof -i :${port} 2>/dev/null`, { timeout: 5000 }).toString().trim()
// ss 输出第一行是表头,需要检查是否有第二行
const lines = output.split('\n').filter(l => l.trim())
if (lines.length > 1 || output.includes(`:${port}`)) {
return `端口 ${port} 已被占用(正在监听)\n${output}`
}
return `端口 ${port} 未被占用(空闲)`
}
} catch {
return `端口 ${port} 未被占用(空闲)`
}
},
// === AI 助手联网搜索工具 ===
async assistant_web_search({ query, max_results = 5 }) {
if (!query) throw new Error('搜索关键词不能为空')
try {
// 使用 DuckDuckGo HTML 搜索
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`
const https = require('https')
const http = require('http')
const fetchModule = url.startsWith('https') ? https : http
const html = await new Promise((resolve, reject) => {
const req = fetchModule.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }, timeout: 10000 }, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
// 跟随重定向
const rUrl = res.headers.location.startsWith('http') ? res.headers.location : `https://html.duckduckgo.com${res.headers.location}`
fetchModule.get(rUrl, { headers: { 'User-Agent': 'Mozilla/5.0' }, timeout: 10000 }, (res2) => {
let d = ''; res2.on('data', c => d += c); res2.on('end', () => resolve(d))
}).on('error', reject)
return
}
let data = ''; res.on('data', c => data += c); res.on('end', () => resolve(data))
})
req.on('error', reject)
req.on('timeout', () => { req.destroy(); reject(new Error('搜索超时')) })
})
// 解析搜索结果
const results = []
const regex = /<a[^>]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi
let match
while ((match = regex.exec(html)) !== null && results.length < max_results) {
const rawUrl = match[1]
const title = match[2].replace(/<[^>]+>/g, '').trim()
const snippet = match[3].replace(/<[^>]+>/g, '').trim()
// DuckDuckGo 的 URL 需要解码
let finalUrl = rawUrl
try {
const uddg = new URL(rawUrl, 'https://duckduckgo.com').searchParams.get('uddg')
if (uddg) finalUrl = decodeURIComponent(uddg)
} catch {}
if (title && finalUrl) {
results.push({ title, url: finalUrl, snippet })
}
}
if (results.length === 0) {
return `搜索「${query}」未找到相关结果。`
}
let output = `搜索「${query}」找到 ${results.length} 条结果:\n\n`
results.forEach((r, i) => {
output += `${i + 1}. **${r.title}**\n ${r.url}\n ${r.snippet}\n\n`
})
return output
} catch (err) {
return `搜索失败: ${err.message}。请检查网络连接。`
}
},
async assistant_fetch_url({ url }) {
if (!url) throw new Error('URL 不能为空')
if (!url.startsWith('http://') && !url.startsWith('https://')) throw new Error('URL 必须以 http:// 或 https:// 开头')
try {
// 优先使用 Jina Reader API免费返回 Markdown
const jinaUrl = 'https://r.jina.ai/' + url
const https = require('https')
const content = await new Promise((resolve, reject) => {
const req = https.get(jinaUrl, {
headers: { 'User-Agent': 'Mozilla/5.0', 'Accept': 'text/plain' },
timeout: 15000,
}, (res) => {
let data = ''
res.on('data', c => {
data += c
if (data.length > 100000) { req.destroy(); resolve(data.slice(0, 100000) + '\n\n[内容已截断,超过 100KB 限制]') }
})
res.on('end', () => resolve(data))
})
req.on('error', reject)
req.on('timeout', () => { req.destroy(); reject(new Error('抓取超时')) })
})
return content || '(页面内容为空)'
} catch (err) {
return `抓取失败: ${err.message}`
}
},
// === 面板配置Web 模式) ===
read_panel_config() {
return readPanelConfig()
},
write_panel_config({ config }) {
if (!fs.existsSync(OPENCLAW_DIR)) fs.mkdirSync(OPENCLAW_DIR, { recursive: true })
fs.writeFileSync(PANEL_CONFIG_PATH, JSON.stringify(config, null, 2))
invalidateConfigCache()
return true
},
// === 扩展工具操作Web 模式) ===
cftunnel_action({ action }) {
const bin = isWindows ? 'cftunnel.exe' : 'cftunnel'
const cmd = action === 'up' ? `${bin} up -d` : `${bin} down`
try {
execSync(cmd, { timeout: 15000, windowsHide: true }).toString()
return true
} catch (e) {
throw new Error(`cftunnel ${action} 失败: ${e.stderr?.toString() || e.message}`)
}
},
get_cftunnel_logs({ lines = 20 }) {
const bin = isWindows ? 'cftunnel.exe' : 'cftunnel'
// 优先使用 cftunnel log 命令
try {
return execSync(`${bin} log -n ${lines} 2>&1`, { timeout: 5000, windowsHide: true }).toString()
} catch {}
// 回退:直接读日志文件
const logPath = path.join(homedir(), '.cftunnel', 'cftunnel.log')
if (!fs.existsSync(logPath)) return '暂无日志'
try {
if (!isWindows) {
return execSync(`tail -${lines} "${logPath}" 2>&1`, { timeout: 3000 }).toString()
}
const content = fs.readFileSync(logPath, 'utf8')
return content.split('\n').slice(-lines).join('\n')
} catch {
const content = fs.readFileSync(logPath, 'utf8')
return content.split('\n').slice(-lines).join('\n')
}
},
install_cftunnel() {
try {
let out
if (isWindows) {
out = execSync('powershell -NoProfile -ExecutionPolicy Bypass -Command "$tmp = Join-Path $env:TEMP install-cftunnel.ps1; Invoke-WebRequest -Uri https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.ps1 -OutFile $tmp -UseBasicParsing; & $tmp; Remove-Item $tmp -ErrorAction SilentlyContinue"', { timeout: 120000, windowsHide: true }).toString()
} else {
out = execSync('curl -fsSL https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.sh | bash', { timeout: 120000 }).toString()
}
return `安装完成\n${out.slice(-500)}`
} catch (e) {
throw new Error('安装失败: ' + (e.stderr?.toString() || e.message).slice(-500))
}
},
install_clawapp() {
try {
let out
if (isWindows) {
out = execSync('powershell -NoProfile -ExecutionPolicy Bypass -Command "$tmp = Join-Path $env:TEMP install-clawapp.ps1; Invoke-WebRequest -Uri https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.ps1 -OutFile $tmp -UseBasicParsing; & $tmp -Auto; Remove-Item $tmp -ErrorAction SilentlyContinue"', { timeout: 300000, windowsHide: true }).toString()
} else {
out = execSync('curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.sh | bash', { timeout: 300000 }).toString()
}
return `安装完成\n${out.slice(-500)}`
} catch (e) {
throw new Error('安装失败: ' + (e.stderr?.toString() || e.message).slice(-500))
}
},
// === Agent 管理Web 模式) ===
add_agent({ name, model, workspace }) {
if (!name) throw new Error('Agent 名称不能为空')
const agentsDir = path.join(OPENCLAW_DIR, 'agents')
const agentDir = path.join(agentsDir, name)
if (fs.existsSync(agentDir)) throw new Error(`Agent "${name}" 已存在`)
fs.mkdirSync(agentDir, { recursive: true })
const meta = { id: name, model: model || null, workspace: workspace || null }
fs.writeFileSync(path.join(agentDir, 'agent.json'), JSON.stringify(meta, null, 2))
return true
},
delete_agent({ id }) {
if (!id || id === 'main') throw new Error('不能删除默认 Agent')
const agentDir = path.join(OPENCLAW_DIR, 'agents', id)
if (!fs.existsSync(agentDir)) throw new Error(`Agent "${id}" 不存在`)
fs.rmSync(agentDir, { recursive: true, force: true })
return true
},
update_agent_identity({ id, name, emoji }) {
if (!id) throw new Error('Agent ID 不能为空')
// 写入 openclaw.json 的 agents 配置
if (!fs.existsSync(CONFIG_PATH)) throw new Error('openclaw.json 不存在')
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))
if (!config.agents) config.agents = {}
if (!config.agents.profiles) config.agents.profiles = {}
if (!config.agents.profiles[id]) config.agents.profiles[id] = {}
if (name) config.agents.profiles[id].identityName = name
if (emoji) config.agents.profiles[id].emoji = emoji
fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.bak')
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
return true
},
update_agent_model({ id, model }) {
if (!id) throw new Error('Agent ID 不能为空')
if (!fs.existsSync(CONFIG_PATH)) throw new Error('openclaw.json 不存在')
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))
if (!config.agents) config.agents = {}
if (!config.agents.profiles) config.agents.profiles = {}
if (!config.agents.profiles[id]) config.agents.profiles[id] = {}
config.agents.profiles[id].model = model || null
fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + '.bak')
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2))
return true
},
backup_agent({ id }) {
if (!id) throw new Error('Agent ID 不能为空')
const suffix = id !== 'main' ? `/agents/${id}` : ''
const wsDir = path.join(OPENCLAW_DIR, 'workspace' + suffix)
if (!fs.existsSync(wsDir)) return '工作区为空,无需备份'
if (!fs.existsSync(BACKUPS_DIR)) fs.mkdirSync(BACKUPS_DIR, { recursive: true })
const now = new Date()
const pad = n => String(n).padStart(2, '0')
const name = `agent-${id}-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}.tar`
try {
execSync(`tar -cf "${path.join(BACKUPS_DIR, name)}" -C "${wsDir}" .`, { timeout: 30000 })
return `已备份: ${name}`
} catch (e) {
throw new Error('备份失败: ' + (e.message || e))
}
},
// === 初始设置工具Web 模式) ===
check_node_at_path({ nodeDir }) {
const nodeBin = path.join(nodeDir, isWindows ? 'node.exe' : 'node')
if (!fs.existsSync(nodeBin)) throw new Error(`未在 ${nodeDir} 找到 node`)
try {
const ver = execSync(`"${nodeBin}" --version 2>&1`, { timeout: 5000, windowsHide: true }).toString().trim()
return { installed: true, version: ver, path: nodeBin }
} catch (e) {
throw new Error('node 检测失败: ' + e.message)
}
},
scan_node_paths() {
const results = []
const candidates = isWindows
? ['C:\\Program Files\\nodejs', 'C:\\Program Files (x86)\\nodejs']
: ['/usr/local/bin', '/usr/bin', '/opt/homebrew/bin', path.join(homedir(), '.nvm/versions/node'), path.join(homedir(), '.volta/bin')]
for (const p of candidates) {
const nodeBin = path.join(p, isWindows ? 'node.exe' : 'node')
if (fs.existsSync(nodeBin)) {
try {
const ver = execSync(`"${nodeBin}" --version 2>&1`, { timeout: 5000, windowsHide: true }).toString().trim()
results.push({ path: p, version: ver })
} catch {}
}
}
return results
},
save_custom_node_path({ nodeDir }) {
const cfg = readPanelConfig()
cfg.customNodePath = nodeDir
if (!fs.existsSync(OPENCLAW_DIR)) fs.mkdirSync(OPENCLAW_DIR, { recursive: true })
fs.writeFileSync(PANEL_CONFIG_PATH, JSON.stringify(cfg, null, 2))
invalidateConfigCache()
return true
},
// === 访问密码认证 ===
auth_check() {
const pw = getAccessPassword()

10
src-tauri/Cargo.lock generated
View File

@@ -328,13 +328,14 @@ dependencies = [
[[package]]
name = "clawpanel"
version = "0.5.7"
version = "0.6.0"
dependencies = [
"base64 0.22.1",
"chrono",
"dirs",
"ed25519-dalek",
"rand 0.8.5",
"regex",
"reqwest 0.12.28",
"serde",
"serde_json",
@@ -343,6 +344,7 @@ dependencies = [
"tauri-build",
"tauri-plugin-shell",
"tokio",
"urlencoding",
"zip",
]
@@ -4361,6 +4363,12 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "clawpanel"
version = "0.5.7"
version = "0.6.0"
edition = "2021"
description = "ClawPanel - OpenClaw 可视化管理面板"
authors = ["qingchencloud"]
@@ -28,4 +28,6 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
sha2 = "0.10"
rand = "0.8"
base64 = "0.22"
urlencoding = "2"
regex = "1"
tokio = { version = "1", features = ["process", "time"] }

View File

@@ -357,6 +357,124 @@ async fn get_port_process(port: u16) -> String {
}
}
/// 联网搜索DuckDuckGo HTML
#[tauri::command]
pub async fn assistant_web_search(
query: String,
max_results: Option<usize>,
) -> Result<String, String> {
let max = max_results.unwrap_or(5);
let url = format!(
"https://html.duckduckgo.com/html/?q={}",
urlencoding::encode(&query)
);
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;
let html = client
.get(&url)
.send()
.await
.map_err(|e| format!("搜索请求失败: {e}"))?
.text()
.await
.map_err(|e| format!("读取搜索结果失败: {e}"))?;
// 解析搜索结果
let mut results = Vec::new();
let re_result = regex::Regex::new(
r#"class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)</a>[\s\S]*?class="result__snippet"[^>]*>([\s\S]*?)</a>"#
).unwrap();
for cap in re_result.captures_iter(&html) {
if results.len() >= max {
break;
}
let raw_url = &cap[1];
let title = regex::Regex::new(r"<[^>]+>")
.unwrap()
.replace_all(&cap[2], "")
.trim()
.to_string();
let snippet = regex::Regex::new(r"<[^>]+>")
.unwrap()
.replace_all(&cap[3], "")
.trim()
.to_string();
// 解码 DuckDuckGo 的重定向 URL
let final_url = if let Some(pos) = raw_url.find("uddg=") {
let encoded = &raw_url[pos + 5..];
let end = encoded.find('&').unwrap_or(encoded.len());
urlencoding::decode(&encoded[..end])
.unwrap_or_else(|_| encoded[..end].into())
.to_string()
} else {
raw_url.to_string()
};
if !title.is_empty() && !final_url.is_empty() {
results.push((title, final_url, snippet));
}
}
if results.is_empty() {
return Ok(format!("搜索「{}」未找到相关结果。", query));
}
let mut output = format!("搜索「{}」找到 {} 条结果:\n\n", query, results.len());
for (i, (title, url, snippet)) in results.iter().enumerate() {
output.push_str(&format!(
"{}. **{}**\n {}\n {}\n\n",
i + 1,
title,
url,
snippet
));
}
Ok(output)
}
/// 抓取 URL 内容(通过 Jina Reader API
#[tauri::command]
pub async fn assistant_fetch_url(url: String) -> Result<String, String> {
if !url.starts_with("http://") && !url.starts_with("https://") {
return Err("URL 必须以 http:// 或 https:// 开头".into());
}
let jina_url = format!("https://r.jina.ai/{}", url);
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0")
.timeout(std::time::Duration::from_secs(15))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;
let content = client
.get(&jina_url)
.header("Accept", "text/plain")
.send()
.await
.map_err(|e| format!("抓取失败: {e}"))?
.text()
.await
.map_err(|e| format!("读取内容失败: {e}"))?;
if content.len() > 100_000 {
Ok(format!(
"{}\n\n[内容已截断,超过 100KB 限制]",
&content[..100_000]
))
} else if content.is_empty() {
Ok("(页面内容为空)".into())
} else {
Ok(content)
}
}
/// 列出目录内容
#[tauri::command]
pub async fn assistant_list_dir(path: String) -> Result<String, String> {

View File

@@ -560,12 +560,16 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
let stdout = child.stdout.take();
// stderr 每行递增进度10→80 区间),让用户看到进度在动
// 同时收集 stderr 用于失败时返回给前端诊断
let app2 = app.clone();
let stderr_lines = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let stderr_lines2 = stderr_lines.clone();
let handle = std::thread::spawn(move || {
let mut progress: u32 = 15;
if let Some(pipe) = stderr {
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
let _ = app2.emit("upgrade-log", &line);
stderr_lines2.lock().unwrap().push(line);
if progress < 75 {
progress += 2;
let _ = app2.emit("upgrade-progress", progress);
@@ -587,8 +591,13 @@ pub async fn upgrade_openclaw(app: tauri::AppHandle, source: String) -> Result<S
let _ = app.emit("upgrade-progress", 100);
if !status.success() {
let _ = app.emit("upgrade-log", "❌ 升级失败");
return Err("升级失败,请查看日志".into());
let code = status.code().map(|c| c.to_string()).unwrap_or("unknown".into());
let _ = app.emit("upgrade-log", format!("升级失败 (exit code: {code})"));
// 把 stderr 最后 15 行带进错误消息,确保前端诊断函数能匹配到
// npm 内部错误码(如 -4058 ENOENT、EPERM 等)
let tail = stderr_lines.lock().unwrap()
.iter().rev().take(15).rev().cloned().collect::<Vec<_>>().join("\n");
return Err(format!("升级失败exit code: {code}\n{tail}"));
}
// 安装成功后再卸载旧包(确保 CLI 始终可用)
@@ -1024,6 +1033,24 @@ pub async fn restart_gateway() -> Result<String, String> {
reload_gateway().await
}
/// 清理 base URL去掉尾部斜杠和已知端点路径防止用户粘贴完整端点 URL 导致路径重复
fn normalize_base_url(raw: &str) -> String {
let mut base = raw.trim_end_matches('/').to_string();
for suffix in &[
"/chat/completions",
"/completions",
"/responses",
"/messages",
"/models",
] {
if base.ends_with(suffix) {
base.truncate(base.len() - suffix.len());
break;
}
}
base.trim_end_matches('/').to_string()
}
/// 测试模型连通性:向 provider 发送一个简单的 chat completion 请求
#[tauri::command]
pub async fn test_model(
@@ -1031,7 +1058,7 @@ pub async fn test_model(
api_key: String,
model_id: String,
) -> Result<String, String> {
let url = format!("{}/chat/completions", base_url.trim_end_matches('/'));
let url = format!("{}/chat/completions", normalize_base_url(&base_url));
let body = serde_json::json!({
"model": model_id,
@@ -1100,7 +1127,7 @@ pub async fn test_model(
/// 获取服务商的远程模型列表(调用 /models 接口)
#[tauri::command]
pub async fn list_remote_models(base_url: String, api_key: String) -> Result<Vec<String>, String> {
let url = format!("{}/models", base_url.trim_end_matches('/'));
let url = format!("{}/models", normalize_base_url(&base_url));
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))

View File

@@ -364,13 +364,13 @@ echo "安装完成"
let mut child = {
let install_script = r#"
$ErrorActionPreference = 'Stop'
$binDir = Join-Path $env:USERPROFILE 'bin'
if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
Write-Output '下载 cftunnel...'
$url = 'https://github.com/qingchencloud/cftunnel/releases/latest/download/cftunnel-windows-amd64.exe'
$dest = Join-Path $binDir 'cftunnel.exe'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
Write-Output '通过官方安装脚本安装 cftunnel...'
$tmp = Join-Path $env:TEMP 'install-cftunnel.ps1'
Write-Output '下载安装脚本到临时文件...'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.ps1' -OutFile $tmp -UseBasicParsing
Write-Output '执行安装脚本...'
& $tmp
Remove-Item $tmp -ErrorAction SilentlyContinue
Write-Output '安装完成'
"#;
// 使用完整路径调用 PowerShell避免 MSYS2/Git Bash 环境下找不到
@@ -434,7 +434,7 @@ Write-Output '安装完成'
Ok("安装成功".into())
}
/// 一键安装 ClawApp通过 npm
/// 一键安装 ClawApp通过官方安装脚本
#[tauri::command]
pub async fn install_clawapp(app: tauri::AppHandle) -> Result<String, String> {
use std::io::{BufRead, BufReader};
@@ -444,25 +444,58 @@ pub async fn install_clawapp(app: tauri::AppHandle) -> Result<String, String> {
let _ = app.emit("install-log", "开始安装 ClawApp...");
let _ = app.emit("install-progress", 10);
let _ = app.emit("install-log", "通过 npm 安装 clawapp...");
let _ = app.emit("install-log", "下载安装脚本...");
let _ = app.emit("install-progress", 30);
#[cfg(target_os = "windows")]
#[cfg(not(target_os = "windows"))]
let mut child = {
let mut cmd = Command::new("cmd");
cmd.args(["/c", "npm", "install", "-g", "clawapp"]);
cmd.creation_flags(0x08000000);
cmd.stdout(Stdio::piped())
let install_script = r#"
#!/bin/bash
set -e
echo "通过官方安装脚本安装 ClawApp..."
curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.sh | bash
echo "安装完成"
"#;
Command::new("bash")
.arg("-c")
.arg(install_script)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("启动安装进程失败: {e}"))?
};
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "windows")]
let mut child = {
Command::new("npm")
.args(["install", "-g", "clawapp"])
.stdout(Stdio::piped())
let install_script = r#"
$ErrorActionPreference = 'Stop'
Write-Output '通过官方安装脚本安装 ClawApp...'
$tmp = Join-Path $env:TEMP 'install-clawapp.ps1'
Write-Output '下载安装脚本到临时文件...'
Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.ps1' -OutFile $tmp -UseBasicParsing
Write-Output '执行安装脚本...'
& $tmp -Auto
Remove-Item $tmp -ErrorAction SilentlyContinue
Write-Output '安装完成'
"#;
let ps_path = std::env::var("SystemRoot")
.map(|root| {
format!(
"{}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
root
)
})
.unwrap_or_else(|_| "powershell.exe".to_string());
let mut cmd = Command::new(&ps_path);
cmd.args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
install_script,
]);
cmd.creation_flags(0x08000000);
cmd.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("启动安装进程失败: {e}"))?

View File

@@ -53,7 +53,12 @@ fn build_enhanced_path() -> String {
format!("{}/.volta/bin", home.display()),
format!("{}/.nodenv/shims", home.display()),
format!("{}/n/bin", home.display()),
format!("{}/.npm-global/bin", home.display()),
];
// NPM_CONFIG_PREFIX: 用户通过 npm config set prefix 自定义的全局安装路径
if let Ok(prefix) = std::env::var("NPM_CONFIG_PREFIX") {
extra.push(format!("{}/bin", prefix));
}
// 扫描 nvm 实际安装的版本目录(兼容无 current 符号链接的情况)
let nvm_versions = home.join(".nvm/versions/node");
if nvm_versions.is_dir() {
@@ -104,7 +109,12 @@ fn build_enhanced_path() -> String {
format!("{}/.volta/bin", home.display()),
format!("{}/.nodenv/shims", home.display()),
format!("{}/n/bin", home.display()),
format!("{}/.npm-global/bin", home.display()),
];
// NPM_CONFIG_PREFIX: 用户通过 npm config set prefix 自定义的全局安装路径
if let Ok(prefix) = std::env::var("NPM_CONFIG_PREFIX") {
extra.push(format!("{}/bin", prefix));
}
// NVM_DIR 环境变量(用户可能自定义了 nvm 安装目录)
let nvm_dir = std::env::var("NVM_DIR")
.ok()

View File

@@ -265,16 +265,37 @@ mod platform {
#[cfg(target_os = "windows")]
mod platform {
use std::os::windows::process::CommandExt;
use std::sync::Mutex;
use tokio::process::Command as TokioCommand;
/// 缓存 is_cli_installed 结果,避免每 15 秒 polling 都 spawn cmd.exe
static CLI_CACHE: Mutex<Option<(bool, std::time::Instant)>> = Mutex::new(None);
const CLI_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60);
/// Windows 不需要 UID
pub fn current_uid() -> Result<u32, String> {
Ok(0)
}
/// 检测 openclaw CLI 是否已安装
/// 检测 openclaw CLI 是否已安装(带 60s 缓存,避免频繁 spawn 进程)
pub fn is_cli_installed() -> bool {
// 方式1: 检查常见文件路径
// 检查缓存
if let Ok(guard) = CLI_CACHE.lock() {
if let Some((val, ts)) = *guard {
if ts.elapsed() < CLI_CACHE_TTL {
return val;
}
}
}
let result = check_cli_installed_inner();
if let Ok(mut guard) = CLI_CACHE.lock() {
*guard = Some((result, std::time::Instant::now()));
}
result
}
fn check_cli_installed_inner() -> bool {
// 方式1: 检查常见文件路径(零进程,最快)
if let Ok(appdata) = std::env::var("APPDATA") {
let cmd_path = std::path::Path::new(&appdata)
.join("npm")

View File

@@ -84,6 +84,8 @@ pub fn run() {
assistant::assistant_system_info,
assistant::assistant_list_processes,
assistant::assistant_check_port,
assistant::assistant_web_search,
assistant::assistant_fetch_url,
// 数据目录 & 图片存储
assistant::assistant_ensure_data_dir,
assistant::assistant_save_image,

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "ClawPanel",
"version": "0.5.7",
"version": "0.6.0",
"identifier": "ai.openclaw.clawpanel",
"build": {
"frontendDist": "../dist",

211
src/components/ai-drawer.js Normal file
View File

@@ -0,0 +1,211 @@
/**
* 全局 AI 助手浮动按钮FAB
* 右下角可拖动按钮 → 点击导航到 AI 助手页面(复用完整功能)
* 自动注入当前页面上下文到 AI 助手会话
*/
const BOT_ICON = '<svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect x="5" y="8" width="14" height="12" rx="2"/><path d="M9 13h0"/><path d="M15 13h0"/><path d="M10 17h4"/></svg>'
const POS_KEY = 'clawpanel-fab-pos'
// ── 页面上下文收集器注册表 ──
const _contextProviders = {}
/**
* 注册页面上下文提供器
* @param {string} route - 路由路径,如 '/chat-debug'
* @param {function} provider - 返回 { label, detail } 的函数(可 async
*/
export function registerPageContext(route, provider) {
_contextProviders[route] = provider
}
// ── 单例 ──
let _fab = null
/** 初始化 FAB */
export function initAIFab() {
if (_fab) return _fab
_fab = createFab()
return _fab
}
/** 导航到 AI 助手并注入错误上下文(显示为可操作的 banner而非自动发送 */
export function openAIDrawerWithError(errorCtx) {
sessionStorage.setItem('assistant-error-context', JSON.stringify({
scene: errorCtx.scene || '',
title: errorCtx.title || '操作失败',
hint: errorCtx.hint || '',
error: truncate(errorCtx.error || '', 3000),
ts: Date.now(),
}))
// 不自动导航 — FAB 按钮会出现红点提示,用户主动点击时跳转
// 如果用户已在助手页,也会实时检测到
if (getCurrentRoute() !== '/assistant') {
// 让 FAB 显示红点
if (_fab?.el) _fab.el.classList.add('has-error')
} else {
// 已在助手页 → 直接触发 banner 显示
window.dispatchEvent(new CustomEvent('assistant-error-injected'))
}
}
function truncate(str, max) {
if (!str || str.length <= max) return str
return str.slice(0, max) + '\n... (截断)'
}
// ── 创建 FAB ──
function createFab() {
const fab = document.createElement('button')
fab.className = 'ai-fab'
fab.title = 'AI 助手'
fab.innerHTML = BOT_ICON
document.body.appendChild(fab)
// 恢复保存的位置
restorePosition(fab)
// ── 拖动逻辑 ──
let _dragging = false
let _dragMoved = false
let _startX = 0, _startY = 0
let _fabX = 0, _fabY = 0
function onPointerDown(e) {
if (e.button !== 0) return
_dragging = true
_dragMoved = false
_startX = e.clientX
_startY = e.clientY
const rect = fab.getBoundingClientRect()
_fabX = rect.left
_fabY = rect.top
fab.style.transition = 'none'
fab.setPointerCapture(e.pointerId)
e.preventDefault()
}
function onPointerMove(e) {
if (!_dragging) return
const dx = e.clientX - _startX
const dy = e.clientY - _startY
if (!_dragMoved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return
_dragMoved = true
fab.classList.add('dragging')
// 计算新位置(限制在视口内)
const vw = window.innerWidth
const vh = window.innerHeight
const size = 48
let newX = Math.max(8, Math.min(vw - size - 8, _fabX + dx))
let newY = Math.max(8, Math.min(vh - size - 8, _fabY + dy))
fab.style.left = newX + 'px'
fab.style.top = newY + 'px'
fab.style.right = 'auto'
fab.style.bottom = 'auto'
}
function onPointerUp(e) {
if (!_dragging) return
_dragging = false
fab.classList.remove('dragging')
fab.style.transition = ''
if (_dragMoved) {
// 吸附到最近的边(左/右)
const rect = fab.getBoundingClientRect()
const vw = window.innerWidth
const vh = window.innerHeight
const snapRight = rect.left > vw / 2
const y = Math.max(8, Math.min(vh - 56, rect.top))
if (snapRight) {
fab.style.left = 'auto'
fab.style.right = '24px'
} else {
fab.style.left = '24px'
fab.style.right = 'auto'
}
fab.style.top = y + 'px'
fab.style.bottom = 'auto'
// 保存位置
savePosition(snapRight ? 'right' : 'left', y)
} else {
// 没有拖动 → 点击
handleClick()
}
}
fab.addEventListener('pointerdown', onPointerDown)
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
// ── 点击 → 导航到 AI 助手 ──
async function handleClick() {
const route = getCurrentRoute()
// 已经在 AI 助手页面,不做任何操作
if (route === '/assistant') return
// 清除红点
fab.classList.remove('has-error')
// 如果没有错误上下文待处理,收集当前页面上下文
if (!sessionStorage.getItem('assistant-error-context')) {
const provider = _contextProviders[route]
if (provider) {
try {
const ctx = await provider()
if (ctx?.detail) {
const prompt = `以下是当前页面的上下文信息,请根据情况提供帮助:\n\n${ctx.detail}`
sessionStorage.setItem('assistant-auto-prompt', prompt)
}
} catch (e) {
console.warn('[ai-fab] 上下文收集失败:', e)
}
}
}
window.location.hash = '#/assistant'
}
// ── 路由变化时隐藏/显示 ──
function updateVisibility() {
const route = getCurrentRoute()
fab.style.display = route === '/assistant' ? 'none' : 'flex'
}
window.addEventListener('hashchange', updateVisibility)
updateVisibility()
return { el: fab }
}
function getCurrentRoute() {
return (window.location.hash.replace('#', '') || '/dashboard').split('?')[0]
}
function savePosition(side, top) {
try {
localStorage.setItem(POS_KEY, JSON.stringify({ side, top }))
} catch {}
}
function restorePosition(fab) {
try {
const raw = localStorage.getItem(POS_KEY)
if (!raw) return
const { side, top } = JSON.parse(raw)
if (side === 'left') {
fab.style.left = '24px'
fab.style.right = 'auto'
}
if (typeof top === 'number') {
fab.style.top = top + 'px'
fab.style.bottom = 'auto'
}
} catch {}
}

View File

@@ -163,6 +163,7 @@ export function showUpgradeModal() {
const text = overlay.querySelector('.upgrade-progress-text')
const logBox = overlay.querySelector('.upgrade-log-box')
const closeBtn = overlay.querySelector('[data-action="close"]')
const _logLines = []
closeBtn.onclick = () => overlay.remove()
overlay.addEventListener('keydown', (e) => {
@@ -171,11 +172,20 @@ export function showUpgradeModal() {
return {
appendLog(line) {
_logLines.push(line)
const div = document.createElement('div')
div.textContent = line
logBox.appendChild(div)
logBox.scrollTop = logBox.scrollHeight
},
appendHtmlLog(line) {
_logLines.push(line)
const div = document.createElement('div')
div.innerHTML = line
logBox.appendChild(div)
logBox.scrollTop = logBox.scrollHeight
},
getLogText() { return _logLines.join('\n') },
setProgress(pct) {
fill.style.width = pct + '%'
if (pct >= 100) text.textContent = '完成'

View File

@@ -3,13 +3,17 @@
* 解析 npm 错误信息,返回用户友好的提示和修复建议
*/
const NPM_CMD = 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com'
/**
* @param {string} errStr - npm 错误输出
* @param {string} errStr - npm 错误输出(可含流式日志)
* @returns {{ title: string, hint?: string, command?: string }}
*/
export function diagnoseInstallError(errStr) {
const s = errStr.toLowerCase()
// ===== 1. Git 相关 =====
// git SSH 权限问题(有 git 但没配 SSH Key
if (s.includes('permission denied (publickey)') || s.includes('ssh://git@github')) {
return {
@@ -28,12 +32,44 @@ export function diagnoseInstallError(errStr) {
}
}
// EPERM文件被占用/权限问题)
// ===== 2. 文件 / 权限 =====
// EPERM文件被占用/权限问题)— 放在 ENOENT 前面,优先匹配
if (s.includes('eperm') || s.includes('operation not permitted')) {
return {
title: '安装失败 — 文件被占用',
hint: '有文件被锁定无法写入。先关闭所有 ClawPanel 和 Node.js 进程,然后在管理员终端手动安装:',
command: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
title: '安装失败 — 文件被占用或权限不足',
hint: '常见原因杀毒软件拦截、Gateway 进程未关闭、或终端缺少管理员权限。\n请先关闭 Gateway再以管理员身份打开终端手动安装:',
command: NPM_CMD,
}
}
// ENOENT文件找不到 / -4058
if (s.includes('enoent') || s.includes('-4058') || s.includes('code -4058')) {
// 尝试从日志中提取具体缺失的路径
const pathMatch = errStr.match(/enoent[^']*'([^']+)'/i) || errStr.match(/path\s+'([^']+)'/i)
const missingPath = pathMatch?.[1] || ''
if (missingPath.includes('node_modules') || missingPath.includes('npm')) {
return {
title: '安装失败 — npm 全局目录异常',
hint: `npm 全局安装目录可能不存在或损坏(${missingPath})。\n请先修复 npm 目录,再重试安装:`,
command: 'npm config set prefix "%APPDATA%\\npm" && ' + NPM_CMD,
}
}
return {
title: '安装失败 — 文件或目录不存在',
hint: '常见原因npm 全局目录未创建、杀毒软件隔离了文件、或磁盘权限问题。\n建议步骤\n1. 关闭杀毒软件的实时防护\n2. 以管理员身份打开 PowerShell\n3. 手动运行安装命令:',
command: NPM_CMD,
}
}
// EACCES权限不足
if (s.includes('eacces') || s.includes('permission denied')) {
const isMac = navigator.platform?.includes('Mac') || navigator.userAgent?.includes('Mac')
return {
title: '安装失败 — 权限不足',
hint: isMac ? '请在终端使用 sudo 安装:' : '请以管理员身份打开 PowerShell 安装:',
command: isMac ? 'sudo ' + NPM_CMD : NPM_CMD,
}
}
@@ -42,53 +78,68 @@ export function diagnoseInstallError(errStr) {
return {
title: '安装不完整',
hint: '上次安装可能中断了。先清理残留再重装:',
command: 'npm cache clean --force && npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
command: 'npm cache clean --force && ' + NPM_CMD,
}
}
// ENOENT文件找不到
if (s.includes('enoent') || s.includes('-4058') || s.includes('code -4058')) {
return {
title: '安装失败 — 文件访问错误',
hint: '尝试以管理员身份运行 ClawPanel或在终端手动安装',
command: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
}
}
// ===== 3. 网络 =====
// 权限不足EACCES / EPERM
if (s.includes('eacces') || s.includes('eperm') || s.includes('permission denied')) {
const isMac = navigator.platform?.includes('Mac') || navigator.userAgent?.includes('Mac')
return {
title: '安装失败 — 权限不足',
hint: isMac ? '请在终端使用 sudo 安装:' : '请以管理员身份打开终端安装:',
command: isMac
? 'sudo npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com'
: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
}
}
// 网络错误
if (s.includes('etimedout') || s.includes('econnrefused') || s.includes('enotfound')
|| s.includes('network') || s.includes('fetch failed') || s.includes('socket hang up')) {
|| s.includes('fetch failed') || s.includes('socket hang up')
|| s.includes('econnreset') || s.includes('unable to get local issuer')) {
const isProxy = s.includes('proxy') || s.includes('unable to get local issuer')
return {
title: '安装失败 — 网络连接错误',
hint: '请检查网络连接,或尝试切换 npm 镜像源后重试。',
hint: isProxy
? '检测到代理/证书问题。如果你使用了 VPN 或公司代理,请尝试关闭后重试,或设置 npm 信任证书:'
: '无法连接到 npm 仓库。请检查网络连接,或尝试使用国内镜像源:',
command: isProxy
? 'npm config set strict-ssl false && ' + NPM_CMD
: NPM_CMD,
}
}
// ===== 4. npm 自身问题 =====
// npm 缓存损坏
if (s.includes('integrity') || s.includes('sha512') || s.includes('cache')) {
return {
title: '安装失败 — npm 缓存异常',
hint: '尝试清理 npm 缓存后重试:',
command: 'npm cache clean --force',
hint: '本地缓存可能损坏。清理缓存后重试:',
command: 'npm cache clean --force && ' + NPM_CMD,
}
}
// 通用 fallback
// Node.js 版本过低
if (s.includes('engine') || s.includes('unsupported') || s.includes('required:')) {
return {
title: '安装失败 — Node.js 版本不兼容',
hint: '当前 Node.js 版本过低OpenClaw 需要 Node.js 18 或更高版本。\n请升级 Node.js',
command: '下载最新版: https://nodejs.org/',
}
}
// npm 版本过低或损坏
if (s.includes('npm err') && (s.includes('cb() never called') || s.includes('code 1'))) {
return {
title: '安装失败 — npm 异常',
hint: 'npm 自身可能异常。尝试更新 npm 后重试:',
command: 'npm install -g npm@latest && ' + NPM_CMD,
}
}
// ===== 5. 磁盘空间 =====
if (s.includes('enospc') || s.includes('no space')) {
return {
title: '安装失败 — 磁盘空间不足',
hint: '磁盘空间不足,请清理磁盘后重试。',
}
}
// ===== fallback =====
return {
title: '安装失败',
hint: '请在终端手动尝试安装,查看完整错误信息:',
command: 'npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmmirror.com',
command: NPM_CMD,
}
}

91
src/lib/icons.js Normal file
View File

@@ -0,0 +1,91 @@
/**
* 统一 SVG 图标库 — 替代所有 Emoji保持视觉一致性
* 基于 Lucide/Feather 风格,使用 currentColor 继承颜色
*/
const PATHS = {
// 状态图标
'check-circle': '<path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>',
'x-circle': '<circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>',
'alert-triangle': '<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>',
'info': '<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>',
// 简单指示符
'check': '<polyline points="20 6 9 17 4 12"/>',
'x': '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
// 操作图标
'search': '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
'gift': '<polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 010-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 000-5C13 2 12 7 12 7z"/>',
'zap': '<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>',
'target': '<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>',
'bar-chart': '<line x1="12" y1="20" x2="12" y2="10"/><line x1="18" y1="20" x2="18" y2="4"/><line x1="6" y1="20" x2="6" y2="16"/>',
'home': '<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
'paperclip': '<path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>',
'clipboard': '<path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>',
'file': '<path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/>',
'file-text': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>',
'file-plain': '<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/>',
'package': '<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
'terminal': '<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>',
'edit': '<path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>',
'folder': '<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>',
'monitor': '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>',
'plug': '<path d="M12 22v-5"/><path d="M9 8V1h6v7"/><path d="M7 8h10a0 0 0 010 0 5 5 0 01-10 0 0 0 0 010 0z"/><path d="M12 8v5"/>',
'wrench': '<path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/>',
'bug': '<path d="M8 2l1.88 1.88M14.12 3.88L16 2M9 7.13v-1a3 3 0 116 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 014-4h4a4 4 0 014 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9M6.53 9C4.6 8.8 3 7.1 3 5M6 13H2M3 21c0-2.1 1.7-3.9 3.8-4M20.97 5c0 2.1-1.6 3.8-3.5 4M22 13h-4M17.2 17c2.1.1 3.8 1.9 3.8 4"/>',
'fire': '<path d="M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z"/>',
'key': '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>',
'lock': '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0110 0v4"/>',
'clock': '<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>',
'send': '<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>',
'download': '<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
'inbox': '<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/>',
'radio': '<circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 010 8.49m-8.48-.01a6 6 0 010-8.49m11.31-2.82a10 10 0 010 14.14m-14.14 0a10 10 0 010-14.14"/>',
'lightbulb': '<line x1="9" y1="18" x2="15" y2="18"/><line x1="10" y1="22" x2="14" y2="22"/><path d="M15.09 14c.18-.98.65-1.74 1.41-2.5A4.65 4.65 0 0018 8 6 6 0 006 8c0 1 .23 2.23 1.5 3.5A4.61 4.61 0 018.91 14"/>',
'globe': '<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/>',
'shield': '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>',
'list': '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
}
/**
* 生成内联 SVG 图标
* @param {string} name 图标名称
* @param {number} [size=16] 图标尺寸px
* @param {string} [className] 可选 CSS 类名
* @returns {string} SVG HTML 字符串
*/
export function icon(name, size = 16, className) {
const paths = PATHS[name]
if (!paths) return ''
const cls = className ? ` class="${className}"` : ''
return `<svg${cls} width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-0.125em;flex-shrink:0">${paths}</svg>`
}
/**
* 状态图标(带颜色)
* @param {'ok'|'err'|'warn'|'info'} type 状态类型
* @param {number} [size=16] 图标尺寸
* @returns {string} 带颜色的 SVG 字符串
*/
export function statusIcon(type, size = 16) {
const map = {
ok: { name: 'check-circle', color: 'var(--success)' },
err: { name: 'x-circle', color: 'var(--danger, var(--error))' },
warn: { name: 'alert-triangle', color: 'var(--warning)' },
info: { name: 'info', color: 'var(--info, var(--primary))' },
}
const cfg = map[type]
if (!cfg) return ''
const paths = PATHS[cfg.name]
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="${cfg.color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-0.125em;flex-shrink:0">${paths}</svg>`
}
/**
* 用于日志前缀的图标(纯文本环境也能回退)
* @param {string} name 图标名称
* @returns {string} 小尺寸 SVG 字符串
*/
export function logIcon(name, size = 14) {
return icon(name, size)
}

254
src/lib/openclaw-kb.js Normal file
View File

@@ -0,0 +1,254 @@
/**
* OpenClaw 内置知识库
* 来源https://openclawcn.com/docs/
* 供 ClawPanel AI 助手在系统提示词中使用
*/
export const OPENCLAW_KB = `
# OpenClaw 知识库(内置参考)
## 一、架构概览
OpenClaw 是开源个人 AI 助手平台,核心组件:
- **Gateway 网关**核心后端服务处理消息路由、Agent 执行、渠道连接
- **CLI**:命令行工具,用于安装/配置/管理 OpenClaw
- **Agent智能体**:独立的 AI 角色实例,有自己的工作区、身份、模型配置
- **Workspace工作区**Agent 的个性化存储Skills、提示、记忆
- **Channel渠道**消息通道WhatsApp/Telegram/Discord/Mattermost 等)
- **Control UI / Dashboard**:内置 Web 管理界面,端口 18789
## 二、目录结构
\`\`\`
~/.openclaw/
├── openclaw.json # 主配置文件JSON5支持注释
├── .env # 全局环境变量
├── workspace/ # 默认(main) Agent 的工作区
│ ├── IDENTITY.md # Agent 身份定义
│ ├── SOUL.md # Agent 灵魂/人格
│ ├── USER.md # 用户信息
│ ├── AGENTS.md # 操作规则
│ └── ... # Skills、记忆等
├── agents/
│ ├── main/
│ │ └── agent/
│ │ ├── auth-profiles.json # 认证配置OAuth + API Key
│ │ ├── models.json # 模型提供商配置
│ │ └── auth.json # 运行时认证缓存(自动管理)
│ └── <agentId>/
│ ├── agent/ # 同上
│ └── workspace/ # 自定义 Agent 的工作区
├── credentials/
│ ├── oauth.json # 旧版 OAuth 导入
│ ├── whatsapp/<accountId>/ # WhatsApp 凭证
│ └── <channel>-allowFrom.json # 配对白名单
└── logs/ # 日志文件
\`\`\`
**重要路径规则:**
- main Agent 工作区:\`~/.openclaw/workspace\`(根级别)
- 自定义 Agent 工作区:\`~/.openclaw/agents/<agentId>/workspace\`
- Agent 配置目录:\`~/.openclaw/agents/<agentId>/agent/\`
## 三、CLI 常用命令
| 命令 | 说明 |
|------|------|
| \`openclaw onboard\` | 新手引导向导(推荐首次使用) |
| \`openclaw onboard --install-daemon\` | 引导 + 安装后台服务 |
| \`openclaw setup\` | 初始化/配置工作区 |
| \`openclaw gateway\` | 启动 Gateway前台 |
| \`openclaw gateway --port 18789 --verbose\` | 指定端口启动 |
| \`openclaw gateway status\` | 查看 Gateway 状态 |
| \`openclaw dashboard\` | 打开 Web Dashboard |
| \`openclaw status\` | 系统状态概览 |
| \`openclaw status --all\` | 完整调试报告(可粘贴) |
| \`openclaw health\` | 健康检查 |
| \`openclaw doctor\` | 诊断配置问题 |
| \`openclaw doctor --fix\` | 自动修复配置问题 |
| \`openclaw security audit --deep\` | 深度安全审计 |
| \`openclaw channels login\` | 登录渠道(如 WhatsApp QR |
| \`openclaw pairing list <channel>\` | 列出配对请求 |
| \`openclaw pairing approve <channel> <code>\` | 批准配对 |
| \`openclaw configure --section web\` | 配置 Web 搜索Brave API |
| \`openclaw config set <key> <value>\` | 设置单个配置项 |
| \`openclaw logs\` | 查看日志 |
| \`openclaw service start/stop/restart\` | 管理后台服务 |
| \`openclaw message send --target <num> --message "text"\` | 发送测试消息 |
## 四、配置文件openclaw.json
配置位于 \`~/.openclaw/openclaw.json\`JSON5 格式(支持注释和尾逗号)。
不存在时使用安全默认值。严格 schema 验证,未知键会阻止启动。
### 最小配置示例
\`\`\`json5
{
agents: {
defaults: {
workspace: "~/.openclaw/workspace"
}
},
channels: {
whatsapp: {
allowFrom: ["+15555550123"]
}
}
}
\`\`\`
### 关键配置项
- **agents.defaults.workspace** — 默认工作区路径
- **agents.defaults.model.primary** — 默认模型(格式 "provider/model"
- **agents.defaults.sandbox** — 沙箱配置mode: "off"|"non-main"|"all"
- **agents.list[]** — 多 Agent 配置id, name, workspace, model, identity, groupChat, sandbox
- **channels.whatsapp** — WhatsAppallowFrom, groups, dmPolicy, accounts
- **channels.telegram** — Telegram Bot
- **channels.discord** — Discord Bot
- **channels.mattermost** — Mattermost 插件
- **gateway.auth.token** — Gateway 认证令牌
- **gateway.port** — Gateway 端口(默认 18789
- **models.providers** — 自定义模型提供商baseUrl, apiKey, api, models[]
- **env.vars** — 内联环境变量
- **bindings[]** — 消息路由绑定channel→agentId
### 配置管理 RPC
- \`config.get\` — 获取当前配置(含 hash
- \`config.apply\` — 全量替换配置并重启(需 baseHash
- \`config.patch\` — 部分更新配置并重启JSON merge patch 语义)
- \`config.schema\` — 获取配置的 JSON Schema
### 环境变量
- \`~/.openclaw/.env\` — 全局 .env
- 配置中支持 \`\${VAR_NAME}\` 语法引用环境变量
- env.shellEnv.enabled=true 可从 shell 导入环境变量
## 五、多 Agent 路由
\`\`\`json5
{
agents: {
list: [
{ id: "main", workspace: "~/.openclaw/workspace", sandbox: { mode: "off" } },
{ id: "helper", name: "Helper Bot", workspace: "~/.openclaw/agents/helper/workspace" }
]
},
bindings: [
{ match: { channel: "telegram" }, agentId: "helper" },
{ match: { channel: "whatsapp" }, agentId: "main" }
]
}
\`\`\`
- main Agent 的工作区默认 \`~/.openclaw/workspace\`
- 其他 Agent 默认 \`~/.openclaw/workspace-<agentId>\`
- Agent 配置目录固定为 \`~/.openclaw/agents/<agentId>/agent/\`
## 六、模型配置
模型配置存储在 \`~/.openclaw/agents/<agentId>/agent/models.json\`
也可在 openclaw.json 的 \`models.providers\` 中定义自定义提供商。
自定义提供商示例:
\`\`\`json5
{
models: {
providers: {
"my-proxy": {
baseUrl: "http://localhost:4000/v1",
apiKey: "sk-...",
api: "openai-completions",
models: [
{ id: "gpt-4o", name: "GPT-4o", reasoning: false, input: ["text", "image"],
contextWindow: 128000, maxTokens: 16384 }
]
}
}
},
agents: {
defaults: {
model: { primary: "my-proxy/gpt-4o" }
}
}
}
\`\`\`
## 七、认证
- **OAuth推荐**:通过 \`openclaw onboard\` 设置,支持 Anthropic、OpenAI Codex
- **API Key**:直接在 auth-profiles.json 或环境变量中设置
- **凭证位置**\`~/.openclaw/agents/<agentId>/agent/auth-profiles.json\`
- **旧版导入**\`~/.openclaw/credentials/oauth.json\`
## 八、安装
**macOS/Linux**
\`\`\`bash
curl -fsSL https://openclaw.ai/install.sh | bash
\`\`\`
**WindowsWSL2 推荐):**
\`\`\`powershell
iwr -useb https://openclaw.ai/install.ps1 | iex
\`\`\`
**npm 全局安装:**
\`\`\`bash
npm install -g openclaw@latest
\`\`\`
**前置条件:** Node.js >= 22
## 九、后台服务
- **macOS**launchd 服务openclaw 应用管理)
- **Linux**systemd 用户服务(需 \`sudo loginctl enable-linger $USER\`
- **Windows**WSL2 内运行 Linux 步骤
## 十、渠道配置
### WhatsApp
- \`openclaw channels login\` → 扫描 QR 登录
- 配置 allowFrom 白名单限制私聊
- groups 配置群组行为requireMention 等)
- 首次私信会返回配对码,需 approve
### Telegram
- 使用 Bot Token
- \`channels.telegram.tokenFile\` 或环境变量
- 群组支持 @提及触发
### Discord
- 使用 Bot Token
- 环境变量或配置中设置
- 支持 guild 级别配置
## 十一、故障排查
1. \`openclaw doctor\` — 诊断所有已知问题
2. \`openclaw doctor --fix\` — 自动修复
3. \`openclaw status --all\` — 完整状态报告
4. \`openclaw health\` — 健康检查
5. \`openclaw logs\` — 查看日志
6. 配置验证失败 → Gateway 拒绝启动,仅允许诊断命令
7. WhatsApp 不回消息 → 检查配对是否已 approve
8. 认证错误 → 检查 auth-profiles.json 或重新 \`openclaw onboard\`
## 十二、ClawPanel 公益 AI 接口计划
ClawPanel 项目组正式开放公益 AI 接口测试计划,降低用户接入 AI 能力的门槛。
### 核心信息
- **GPT-AI 网关地址**https://gpt.qt.cool/v1
- **支持接口**/v1/chat/completions、/v1/responsesOpenAI 兼容)
- **支持模型**OpenAI 全系列GPT-5、GPT-5.1、GPT-5.2、GPT-5.3-codex、GPT-5.4 等)
- **费用**:测试期间所有调用成本由项目组内部承担,用户无需付费
- **限制**:无请求限制
- **用户后台**https://gpt.qt.cool/user查看用量、提交工单
### 官方入口
- **ClawPanel 官网**https://claw.qt.cool/
- **GPT-AI 网关官网**https://gpt.qt.cool/
- **用户后台**https://gpt.qt.cool/user
- **晴辰导航站**https://qt.cool/
### 测试密钥
- ClawPanel 已内置公共体验密钥,开箱即用
- 用户也可前往 gpt.qt.cool 签到获取独立密钥
- 独立密钥可在用户后台管理和查询用量
### 接入方式
已兼容 OpenAI API 的项目,只需替换:
1. Base URL → https://gpt.qt.cool/v1
2. API Key → 测试密钥
即可完成接入。
### 在 ClawPanel 中配置
- **助手设置**:打开 AI 助手设置 → 模型配置 → 使用「一键接入」按钮
- **模型配置页**:进入模型配置 → 使用「一键添加全部模型」按钮
- 两处均自动填入网关地址和内置密钥
`.trim()

View File

@@ -249,6 +249,8 @@ function mockInvoke(cmd, args) {
assistant_system_info: () => `OS: ${navigator.platform.includes('Win') ? 'windows' : navigator.platform.includes('Mac') ? 'macos' : 'linux'}\nArch: x86_64\nHome: ${navigator.platform.includes('Win') ? 'C:\\Users\\user' : '/Users/user'}\nHostname: mock-host\nShell: ${navigator.platform.includes('Win') ? 'powershell / cmd' : 'zsh'}\nPath separator: ${navigator.platform.includes('Win') ? '\\\\' : '/'}`,
assistant_list_processes: ({ filter }) => filter ? `Id ProcessName\n-- -----------\n1234 ${filter}\n5678 ${filter}-helper` : 'Id ProcessName\n-- -----------\n1 System\n1234 node\n5678 openclaw',
assistant_check_port: ({ port }) => port === 18789 ? `端口 ${port} 已被占用(正在监听)\n占用进程: node` : `端口 ${port} 未被占用(空闲)`,
assistant_web_search: ({ query }) => `搜索「${query}」找到 3 条结果:\n\n1. **${query} - 文档**\n https://example.com/docs\n 这是关于 ${query} 的文档页面\n\n2. **${query} 常见问题**\n https://example.com/faq\n 常见问题解答\n\n3. **${query} GitHub**\n https://github.com/example\n 开源仓库`,
assistant_fetch_url: ({ url }) => `# ${url}\n\n这是从 ${url} 抓取的网页内容mock\n\n## 主要内容\n\n示例文本...`,
// 数据目录 & 图片存储
assistant_ensure_data_dir: () => (navigator.platform.includes('Win') ? 'C:\\Users\\user\\.openclaw\\clawpanel' : '/Users/user/.openclaw/clawpanel'),
assistant_save_image: ({ id }) => `/mock/images/${id}.jpg`,
@@ -347,6 +349,8 @@ export const api = {
assistantSystemInfo: () => invoke('assistant_system_info'),
assistantListProcesses: (filter) => invoke('assistant_list_processes', { filter: filter || null }),
assistantCheckPort: (port) => invoke('assistant_check_port', { port }),
assistantWebSearch: (query, maxResults) => invoke('assistant_web_search', { query, max_results: maxResults || 5 }),
assistantFetchUrl: (url) => invoke('assistant_fetch_url', { url }),
// 数据目录 & 图片存储
ensureDataDir: () => invoke('assistant_ensure_data_dir'),

View File

@@ -8,6 +8,7 @@ import { detectOpenclawStatus, isOpenclawReady, isGatewayRunning, onGatewayChang
import { wsClient } from './lib/ws-client.js'
import { api } from './lib/tauri-api.js'
import { version as APP_VERSION } from '../package.json'
import { statusIcon } from './lib/icons.js'
// 样式
import './style/variables.css'
@@ -19,6 +20,7 @@ import './style/chat.css'
import './style/agents.css'
import './style/debug.css'
import './style/assistant.css'
import './style/ai-drawer.css'
// 初始化主题
initTheme()
@@ -183,7 +185,7 @@ async function boot() {
banner.id = 'pw-change-banner'
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;padding:10px 20px;display:flex;align-items:center;justify-content:center;gap:12px;font-size:13px;font-weight:500;box-shadow:0 2px 8px rgba(0,0,0,0.15)'
banner.innerHTML = `
<span>⚠️ 当前使用的是系统生成的默认密码,为了安全请尽快修改</span>
<span>${statusIcon('warn', 14)} 当前使用的是系统生成的默认密码,为了安全请尽快修改</span>
<a href="#/security" style="color:#fff;background:rgba(255,255,255,0.2);padding:4px 14px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600" onclick="document.getElementById('pw-change-banner').remove();sessionStorage.removeItem('clawpanel_must_change_pw')">前往安全设置</a>
<button onclick="this.parentElement.remove()" style="background:none;border:none;color:rgba(255,255,255,0.7);cursor:pointer;font-size:16px;padding:0 4px;margin-left:4px">✕</button>
`
@@ -286,7 +288,7 @@ function setupGatewayBanner() {
banner.classList.remove('gw-banner-hidden')
banner.innerHTML = `
<div class="gw-banner-content">
<span class="gw-banner-icon"></span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>Gateway 未启动,部分功能不可用</span>
<button class="btn btn-sm btn-primary" id="btn-gw-start">启动 Gateway</button>
</div>
@@ -302,7 +304,7 @@ function setupGatewayBanner() {
const errMsg = err.message || String(err)
banner.innerHTML = `
<div class="gw-banner-content">
<span class="gw-banner-icon"></span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>启动失败: ${errMsg}</span>
<button class="btn btn-sm btn-primary" id="btn-gw-start">重试</button>
<a class="btn btn-sm btn-ghost" href="#/logs" style="color:inherit;text-decoration:underline">查看日志</a>
@@ -331,7 +333,7 @@ function setupGatewayBanner() {
} catch {}
banner.innerHTML = `
<div class="gw-banner-content">
<span class="gw-banner-icon"></span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>启动超时Gateway 可能仍在启动中</span>
<button class="btn btn-sm btn-primary" id="btn-gw-start">重试</button>
<a class="btn btn-sm btn-ghost" href="#/logs" style="color:inherit;text-decoration:underline">查看日志</a>
@@ -353,7 +355,7 @@ function showGuardianRecovery() {
banner.classList.remove('gw-banner-hidden')
banner.innerHTML = `
<div class="gw-banner-content" style="flex-wrap:wrap;gap:8px">
<span class="gw-banner-icon">🛠</span>
<span class="gw-banner-icon">${statusIcon('warn', 16)}</span>
<span>Gateway 反复启动失败,可能配置有误</span>
<button class="btn btn-sm btn-primary" id="btn-gw-recover-restart">重试启动</button>
<button class="btn btn-sm btn-secondary" id="btn-gw-recover-backup">从备份恢复</button>
@@ -384,4 +386,66 @@ function showGuardianRecovery() {
const auth = await checkAuth()
if (!auth.ok) await showLoginOverlay(auth.defaultPw)
boot()
// 初始化全局 AI 助手浮动按钮(延迟加载,不阻塞启动)
setTimeout(async () => {
const { initAIFab, registerPageContext, openAIDrawerWithError } = await import('./components/ai-drawer.js')
initAIFab()
// 注册各页面上下文提供器
registerPageContext('/chat-debug', async () => {
const { isOpenclawReady, isGatewayRunning } = await import('./lib/app-state.js')
const { wsClient } = await import('./lib/ws-client.js')
const { api } = await import('./lib/tauri-api.js')
const lines = ['## 系统诊断快照']
lines.push(`- OpenClaw: ${isOpenclawReady() ? '就绪' : '未就绪'}`)
lines.push(`- Gateway: ${isGatewayRunning() ? '运行中' : '未运行'}`)
lines.push(`- WebSocket: ${wsClient.connected ? '已连接' : '未连接'}`)
try {
const node = await api.checkNode()
lines.push(`- Node.js: ${node?.version || '未知'}`)
} catch {}
try {
const ver = await api.getVersionInfo()
lines.push(`- 版本: ${ver?.current || '?'}${ver?.latest || '?'}`)
} catch {}
return { detail: lines.join('\n') }
})
registerPageContext('/services', async () => {
const { isGatewayRunning } = await import('./lib/app-state.js')
const { api } = await import('./lib/tauri-api.js')
const lines = ['## 服务状态']
lines.push(`- Gateway: ${isGatewayRunning() ? '运行中' : '未运行'}`)
try {
const svc = await api.getServicesStatus()
if (svc?.[0]) {
lines.push(`- CLI: ${svc[0].cli_installed ? '已安装' : '未安装'}`)
lines.push(`- PID: ${svc[0].pid || '无'}`)
}
} catch {}
return { detail: lines.join('\n') }
})
registerPageContext('/gateway', async () => {
const { api } = await import('./lib/tauri-api.js')
try {
const config = await api.readOpenclawConfig()
const gw = config?.gateway || {}
const lines = ['## Gateway 配置']
lines.push(`- 端口: ${gw.port || 18789}`)
lines.push(`- 模式: ${gw.mode || 'local'}`)
lines.push(`- Token: ${gw.auth?.token ? '已设置' : '未设置'}`)
if (gw.controlUi?.allowedOrigins) lines.push(`- Origins: ${JSON.stringify(gw.controlUi.allowedOrigins)}`)
return { detail: lines.join('\n') }
} catch { return null }
})
registerPageContext('/setup', () => {
return { detail: '用户正在进行 OpenClaw 初始安装,请帮助检查 Node.js 环境和网络状况' }
})
// 挂到全局,供安装/升级失败时调用
window.__openAIDrawerWithError = openAIDrawerWithError
}, 500)
})()

View File

@@ -6,6 +6,7 @@ import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showUpgradeModal } from '../components/modal.js'
import { setUpgrading } from '../lib/app-state.js'
import { icon, statusIcon } from '../lib/icons.js'
export async function render() {
const page = document.createElement('div')
@@ -135,8 +136,23 @@ async function loadData(page) {
modal.setDone(typeof msg === 'string' ? msg : (msg?.message || '升级完成'))
loadData(page)
} catch (e) {
modal.appendLog(String(e))
modal.setError('升级失败')
const errStr = String(e)
modal.appendLog(errStr)
const { diagnoseInstallError } = await import('../lib/error-diagnosis.js')
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendHtmlLog(`${statusIcon('info', 14)} ${diagnosis.hint}`)
if (diagnosis.command) modal.appendHtmlLog(`${icon('clipboard', 14)} ${diagnosis.command}`)
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: diagnosis.title,
error: fullLog,
scene: '升级 OpenClaw',
hint: diagnosis.hint,
})
}
} finally {
setUpgrading(false)
unlistenLog?.()
@@ -173,11 +189,16 @@ function renderCommunity(page) {
<img src="/images/OpenClawWx.png" alt="微信交流群" style="width:140px;height:140px;border-radius:var(--radius-md);border:1px solid var(--border-primary)">
<div style="font-size:var(--font-size-sm);margin-top:8px;color:var(--text-secondary)">微信交流群</div>
</div>
<div style="text-align:center">
<img src="https://qt.cool/c/OpenClawDY/qr.png" alt="抖音交流群" style="width:140px;height:140px;border-radius:var(--radius-md);border:1px solid var(--border-primary);object-fit:contain;background:#fff">
<div style="font-size:var(--font-size-sm);margin-top:8px;color:var(--text-secondary)">抖音交流群</div>
</div>
<div style="flex:1;min-width:200px;display:flex;flex-direction:column;gap:8px;padding-top:4px">
<div style="font-size:var(--font-size-sm);color:var(--text-secondary)">扫码或点击链接加入交流群,反馈问题、获取帮助</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px">
<a class="btn btn-primary btn-sm" href="https://qt.cool/c/OpenClaw" target="_blank" rel="noopener">加入 QQ 群</a>
<a class="btn btn-primary btn-sm" href="https://qt.cool/c/OpenClawWx" target="_blank" rel="noopener">加入微信群</a>
<a class="btn btn-primary btn-sm" href="https://qt.cool/c/OpenClawDY" target="_blank" rel="noopener">加入抖音群</a>
<a class="btn btn-secondary btn-sm" href="https://yb.tencent.com/gp/i/LsvIw7mdR7Lb" target="_blank" rel="noopener">元宝派社群</a>
</div>
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary);margin-top:8px">
@@ -194,6 +215,11 @@ const PROJECTS = [
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理',
url: 'https://github.com/openclaw/openclaw',
},
{
name: 'OpenClaw-zh',
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理-中文优化版',
url: 'https://github.com/1186258278/OpenClawChineseTranslation',
},
{
name: 'ClawApp',
desc: '跨平台移动聊天客户端H5 + 代理服务器架构,支持离线和流式传输',

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
import { api, getRequestLogs, clearRequestLogs } from '../lib/tauri-api.js'
import { wsClient } from '../lib/ws-client.js'
import { isOpenclawReady, isGatewayRunning } from '../lib/app-state.js'
import { icon, statusIcon } from '../lib/icons.js'
export async function render() {
const page = document.createElement('div')
@@ -112,7 +113,7 @@ function renderDebugInfo(el, info) {
// 总体状态概览
const allOk = info.appState.openclawReady && info.appState.gatewayRunning && info.wsClient.gatewayReady
html += `<div class="config-section" style="background:${allOk ? 'var(--success-bg)' : 'var(--warning-bg)'};border-left:3px solid ${allOk ? 'var(--success)' : 'var(--warning)'}">
<div style="font-size:16px;font-weight:600;margin-bottom:8px">${allOk ? '✅ 系统正常' : '⚠️ 发现问题'}</div>
<div style="font-size:16px;font-weight:600;margin-bottom:8px">${allOk ? `${statusIcon('ok')} 系统正常` : `${statusIcon('warn')} 发现问题`}</div>
<div style="color:var(--text-secondary);font-size:13px">${allOk ? '所有核心功能运行正常' : '部分功能异常,请查看下方详情'}</div>
</div>`
@@ -120,8 +121,8 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">应用状态</div>
<table class="debug-table">
<tr><td>OpenClaw 就绪</td><td>${info.appState.openclawReady ? '✅' : '❌'}</td></tr>
<tr><td>Gateway 运行中</td><td>${info.appState.gatewayRunning ? '✅' : '❌'}</td></tr>
<tr><td>OpenClaw 就绪</td><td>${info.appState.openclawReady ? statusIcon('ok') : statusIcon('err')}</td></tr>
<tr><td>Gateway 运行中</td><td>${info.appState.gatewayRunning ? statusIcon('ok') : statusIcon('err')}</td></tr>
</table>
</div>`
@@ -129,8 +130,8 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">WebSocket 连接</div>
<table class="debug-table">
<tr><td>连接状态</td><td>${info.wsClient.connected ? '✅ 已连接' : '❌ 未连接'}</td></tr>
<tr><td>握手状态</td><td>${info.wsClient.gatewayReady ? '✅ 已完成' : '❌ 未完成'}</td></tr>
<tr><td>连接状态</td><td>${info.wsClient.connected ? `${statusIcon('ok')} 已连接` : `${statusIcon('err')} 未连接`}</td></tr>
<tr><td>握手状态</td><td>${info.wsClient.gatewayReady ? `${statusIcon('ok')} 已完成` : `${statusIcon('err')} 未完成`}</td></tr>
<tr><td>会话密钥</td><td>${info.wsClient.sessionKey || '(空)'}</td></tr>
</table>
</div>`
@@ -139,10 +140,10 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">Node.js 环境</div>`
if (info.nodeError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.nodeError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${escapeHtml(info.nodeError)}</div>`
} else if (info.node) {
html += `<table class="debug-table">
<tr><td>安装状态</td><td>${info.node.installed ? '✅ 已安装' : '❌ 未安装'}</td></tr>
<tr><td>安装状态</td><td>${info.node.installed ? `${statusIcon('ok')} 已安装` : `${statusIcon('err')} 未安装`}</td></tr>
<tr><td>版本</td><td>${info.node.version || '(未知)'}</td></tr>
</table>`
}
@@ -152,12 +153,12 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">版本信息</div>`
if (info.versionError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.versionError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${escapeHtml(info.versionError)}</div>`
} else if (info.version) {
html += `<table class="debug-table">
<tr><td>当前版本</td><td>${info.version.current || '(未知)'}</td></tr>
<tr><td>最新版本</td><td>${info.version.latest || '(未检测)'}</td></tr>
<tr><td>更新可用</td><td>${info.version.update_available ? '⚠️ 有新版本' : '✅ 已是最新'}</td></tr>
<tr><td>更新可用</td><td>${info.version.update_available ? `${statusIcon('warn')} 有新版本` : `${statusIcon('ok')} 已是最新`}</td></tr>
</table>`
}
html += `</div>`
@@ -166,13 +167,13 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">配置文件</div>`
if (info.configError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.configError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${escapeHtml(info.configError)}</div>`
} else if (info.config) {
const gw = info.config.gateway || {}
html += `<table class="debug-table">
<tr><td>gateway.port</td><td>${gw.port || '(未设置)'}</td></tr>
<tr><td>gateway.auth.token</td><td>${gw.auth?.token ? '✅ 已设置' : '⚠️ 未设置'}</td></tr>
<tr><td>gateway.enabled</td><td>${gw.enabled !== false ? '✅' : '❌'}</td></tr>
<tr><td>gateway.auth.token</td><td>${gw.auth?.token ? `${statusIcon('ok')} 已设置` : `${statusIcon('warn')} 未设置`}</td></tr>
<tr><td>gateway.enabled</td><td>${gw.enabled !== false ? statusIcon('ok') : statusIcon('err')}</td></tr>
<tr><td>gateway.mode</td><td>${gw.mode || 'local'}</td></tr>
</table>`
}
@@ -182,12 +183,12 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">服务状态</div>`
if (info.servicesError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.servicesError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${escapeHtml(info.servicesError)}</div>`
} else if (info.services?.length > 0) {
const svc = info.services[0]
html += `<table class="debug-table">
<tr><td>CLI 安装</td><td>${svc.cli_installed !== false ? '✅ 已安装' : '❌ 未安装'}</td></tr>
<tr><td>运行状态</td><td>${svc.running ? '✅ 运行中' : '❌ 已停止'}</td></tr>
<tr><td>CLI 安装</td><td>${svc.cli_installed !== false ? `${statusIcon('ok')} 已安装` : `${statusIcon('err')} 未安装`}</td></tr>
<tr><td>运行状态</td><td>${svc.running ? `${statusIcon('ok')} 运行中` : `${statusIcon('err')} 已停止`}</td></tr>
<tr><td>进程 PID</td><td>${svc.pid || '(无)'}</td></tr>
<tr><td>服务标签</td><td>${svc.label || '(未知)'}</td></tr>
</table>`
@@ -198,10 +199,10 @@ function renderDebugInfo(el, info) {
html += `<div class="config-section">
<div class="config-section-title">设备密钥 & 握手签名</div>`
if (info.connectFrameError) {
html += `<div style="color:var(--error)"> ${escapeHtml(info.connectFrameError)}</div>`
html += `<div style="color:var(--error)">${statusIcon('err')} ${escapeHtml(info.connectFrameError)}</div>`
} else if (info.connectFrame) {
const device = info.connectFrame.params?.device
html += `<div style="color:var(--success);margin-bottom:8px"> 设备密钥生成成功</div>
html += `<div style="color:var(--success);margin-bottom:8px">${statusIcon('ok')} 设备密钥生成成功</div>
<table class="debug-table">
<tr><td>设备 ID</td><td style="font-size:10px;word-break:break-all">${device?.id || '(无)'}</td></tr>
<tr><td>公钥</td><td style="font-size:10px;word-break:break-all">${device?.publicKey ? device.publicKey.substring(0, 32) + '...' : '(无)'}</td></tr>
@@ -220,31 +221,31 @@ function renderDebugInfo(el, info) {
<ul style="margin:0;padding-left:20px;color:var(--text-secondary);font-size:13px">`
if (!info.node?.installed) {
html += `<li style="color:var(--error);margin-bottom:6px"> Node.js 未安装,请先安装 Node.js<a href="https://nodejs.org/" target="_blank" rel="noopener">下载地址</a></li>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} Node.js 未安装,请先安装 Node.js<a href="https://nodejs.org/" target="_blank" rel="noopener">下载地址</a></li>`
}
if (info.configError) {
html += `<li style="color:var(--error);margin-bottom:6px"> 配置文件不存在或损坏,请前往"初始设置"页面完成配置</li>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} 配置文件不存在或损坏,请前往"初始设置"页面完成配置</li>`
}
if (info.servicesError || !info.services?.length || info.services[0]?.cli_installed === false) {
html += `<li style="color:var(--error);margin-bottom:6px"> OpenClaw CLI 未安装,请前往"初始设置"页面安装</li>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} OpenClaw CLI 未安装,请前往"初始设置"页面安装</li>`
}
if (info.services?.length > 0 && !info.services[0]?.running) {
html += `<li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway 未启动,请前往"服务管理"页面启动服务</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} Gateway 未启动,请前往"服务管理"页面启动服务</li>`
}
if (info.config && !info.config.gateway?.auth?.token) {
html += `<li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway token 未设置(本地开发可选,生产环境建议设置)</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} Gateway token 未设置(本地开发可选,生产环境建议设置)</li>`
}
if (info.connectFrameError) {
html += `<li style="color:var(--error);margin-bottom:6px"> 设备密钥生成失败,请检查 Rust 后端日志</li>`
html += `<li style="color:var(--error);margin-bottom:6px">${statusIcon('err')} 设备密钥生成失败,请检查 Rust 后端日志</li>`
}
if (!info.wsClient.connected && info.services?.length > 0 && info.services[0]?.running) {
html += `<li style="color:var(--warning);margin-bottom:6px">⚠️ Gateway 运行中但 WebSocket 未连接,常见原因:<strong>origin not allowed</strong>Tauri origin 未在白名单)或端口 ${info.config?.gateway?.port || 18789} 被占用。点击“一键修复配对”可自动修复 origin 问题</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} Gateway 运行中但 WebSocket 未连接,常见原因:<strong>origin not allowed</strong>Tauri origin 未在白名单)或端口 ${info.config?.gateway?.port || 18789} 被占用。点击“一键修复配对”可自动修复 origin 问题</li>`
}
if (info.wsClient.connected && !info.wsClient.gatewayReady) {
html += `<li style="color:var(--warning);margin-bottom:6px">⚠️ WebSocket 已连接但握手未完成,请检查 token 是否正确</li>`
html += `<li style="color:var(--warning);margin-bottom:6px">${statusIcon('warn')} WebSocket 已连接但握手未完成,请检查 token 是否正确</li>`
}
if (allOk) {
html += `<li style="color:var(--success);margin-bottom:6px"> 所有检测项正常,系统运行良好</li>`
html += `<li style="color:var(--success);margin-bottom:6px">${statusIcon('ok')} 所有检测项正常,系统运行良好</li>`
}
html += `</ul></div>`
@@ -277,10 +278,10 @@ function testWebSocket(page) {
clearBtn.onclick = () => {
testLogs = []
contentEl.textContent = ''
contentEl.innerHTML = ''
}
addLog('🔍 开始 WebSocket 连接测试...')
addLog(`${icon('search', 14)} 开始 WebSocket 连接测试...`)
// 关闭旧连接
if (testWs) {
@@ -295,86 +296,88 @@ function testWebSocket(page) {
const wsHost = window.__TAURI_INTERNALS__ ? `127.0.0.1:${port}` : location.host
const url = `ws://${wsHost}/ws?token=${encodeURIComponent(token)}`
addLog(`📡 连接地址: ${url}`)
addLog(`🔑 Token: ${token ? token.substring(0, 20) + '...' : '(空)'}`)
addLog(` 正在连接...`)
addLog(`${icon('radio', 14)} 连接地址: ${url}`)
addLog(`${icon('key', 14)} Token: ${token ? token.substring(0, 20) + '...' : '(空)'}`)
addLog(`${icon('clock', 14)} 正在连接...`)
try {
testWs = new WebSocket(url)
testWs.onopen = () => {
addLog('✅ WebSocket 连接成功')
addLog('⏳ 等待 Gateway 发送 connect.challenge...')
addLog(`${statusIcon('ok', 14)} WebSocket 连接成功`)
addLog(`${icon('clock', 14)} 等待 Gateway 发送 connect.challenge...`)
}
testWs.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data)
addLog(`📥 收到消息: ${JSON.stringify(msg, null, 2)}`)
addLog(`${icon('inbox', 14)} 收到消息: ${escapeHtml(JSON.stringify(msg, null, 2))}`)
// 如果收到 challenge尝试发送 connect frame
if (msg.type === 'event' && msg.event === 'connect.challenge') {
const nonce = msg.payload?.nonce || ''
addLog(`🔐 收到 challenge, nonce: ${nonce}`)
addLog(` 生成 connect frame...`)
addLog(`${icon('lock', 14)} 收到 challenge, nonce: ${nonce}`)
addLog(`${icon('clock', 14)} 生成 connect frame...`)
api.createConnectFrame(nonce, token).then(frame => {
addLog(` Connect frame 生成成功`)
addLog(`📤 发送 connect frame: ${JSON.stringify(frame, null, 2)}`)
addLog(`${statusIcon('ok', 14)} Connect frame 生成成功`)
addLog(`${icon('send', 14)} 发送 connect frame: ${escapeHtml(JSON.stringify(frame, null, 2))}`)
testWs.send(JSON.stringify(frame))
}).catch(e => {
addLog(` 生成 connect frame 失败: ${e}`)
addLog(`${statusIcon('err', 14)} 生成 connect frame 失败: ${e}`)
})
}
// 如果收到 connect 响应
if (msg.type === 'res' && msg.id?.startsWith('connect-')) {
if (msg.ok) {
addLog(` 握手成功!`)
addLog(`📊 Snapshot: ${JSON.stringify(msg.payload, null, 2)}`)
addLog(`${statusIcon('ok', 14)} 握手成功!`)
addLog(`${icon('bar-chart', 14)} Snapshot: ${escapeHtml(JSON.stringify(msg.payload, null, 2))}`)
const sessionKey = msg.payload?.snapshot?.sessionDefaults?.mainSessionKey
if (sessionKey) {
addLog(`🔑 Session Key: ${sessionKey}`)
addLog(`${icon('key', 14)} Session Key: ${sessionKey}`)
}
} else {
addLog(` 握手失败: ${msg.error?.message || msg.error?.code || '未知错误'}`)
addLog(`${statusIcon('err', 14)} 握手失败: ${msg.error?.message || msg.error?.code || '未知错误'}`)
}
}
} catch (e) {
addLog(`⚠️ 解析消息失败: ${e}`)
addLog(`📥 原始数据: ${evt.data}`)
addLog(`${statusIcon('warn', 14)} 解析消息失败: ${e}`)
addLog(`${icon('inbox', 14)} 原始数据: ${escapeHtml(evt.data)}`)
}
}
testWs.onerror = (e) => {
addLog(` WebSocket 错误: ${e.type}`)
addLog(`${statusIcon('err', 14)} WebSocket 错误: ${e.type}`)
}
testWs.onclose = (e) => {
addLog(`🔌 连接关闭 - Code: ${e.code}, Reason: ${e.reason || '(空)'}`)
addLog(`${icon('plug', 14)} 连接关闭 - Code: ${e.code}, Reason: ${e.reason || '(空)'}`)
if (e.code === 1008) {
addLog(` origin not allowed (1008) - Gateway 拒绝了当前应用的 origin`)
addLog(`💡 解决方法:点击“一键修复配对”,将自动将 tauri://localhost 加入白名单并重启 Gateway`)
addLog(`${statusIcon('err', 14)} origin not allowed (1008) - Gateway 拒绝了当前应用的 origin`)
addLog(`${icon('lightbulb', 14)} 解决方法:点击“一键修复配对”,将自动将 tauri://localhost 加入白名单并重启 Gateway`)
} else if (e.code === 4001) {
addLog(` 认证失败 (4001) - Token 可能不正确`)
addLog(`${statusIcon('err', 14)} 认证失败 (4001) - Token 可能不正确`)
} else if (e.code === 1006) {
addLog(`⚠️ 异常关闭 (1006) - 可能是网络问题或 Gateway 主动断开`)
addLog(`${statusIcon('warn', 14)} 异常关闭 (1006) - 可能是网络问题或 Gateway 主动断开`)
}
testWs = null
}
} catch (e) {
addLog(` 创建 WebSocket 失败: ${e}`)
addLog(`${statusIcon('err', 14)} 创建 WebSocket 失败: ${e}`)
}
}).catch(e => {
addLog(` 读取配置失败: ${e}`)
addLog(`${statusIcon('err', 14)} 读取配置失败: ${e}`)
})
function addLog(msg) {
const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false })
const line = `[${timestamp}] ${msg}`
testLogs.push(line)
contentEl.textContent = testLogs.join('\n')
const div = document.createElement('div')
div.style.cssText = 'display:flex;gap:4px;align-items:flex-start;padding:1px 0;white-space:pre-wrap;word-break:break-all'
div.innerHTML = `<span style="color:var(--text-tertiary);flex-shrink:0">[${timestamp}]</span> ${msg}`
testLogs.push(div.textContent)
contentEl.appendChild(div)
contentEl.scrollTop = contentEl.scrollHeight
}
}
@@ -440,7 +443,7 @@ function renderNetworkLog(contentEl) {
// 倒序显示(最新的在上面)
for (let i = logs.length - 1; i >= 0; i--) {
const log = logs[i]
const cachedIcon = log.cached ? '✅' : '-'
const cachedIcon = log.cached ? statusIcon('ok', 12) : '-'
const durationColor = log.cached ? 'var(--text-tertiary)' :
(parseInt(log.duration) > 1000 ? 'var(--error)' :
(parseInt(log.duration) > 500 ? 'var(--warning)' : 'var(--text-primary)'))
@@ -480,36 +483,36 @@ async function fixPairing(page) {
}
try {
addLog('🔧 开始修复配对问题...')
addLog(`${icon('wrench', 14)} 开始修复配对问题...`)
// 1. 写入 paired.json + controlUi.allowedOrigins
addLog('📝 正在写入设备配对信息 + Gateway origin 白名单...')
addLog(`${icon('edit', 14)} 正在写入设备配对信息 + Gateway origin 白名单...`)
const result = await api.autoPairDevice()
addLog(` ${result}`)
addLog('✅ 已将 tauri://localhost 加入 gateway.controlUi.allowedOrigins')
addLog(`${statusIcon('ok', 14)} ${result}`)
addLog(`${statusIcon('ok', 14)} 已将 tauri://localhost 加入 gateway.controlUi.allowedOrigins`)
// 2. 重启 Gateway
addLog('🔄 重启 Gateway 服务...')
addLog(`${icon('zap', 14)} 重启 Gateway 服务...`)
await api.restartService('ai.openclaw.gateway')
addLog('✅ Gateway 重启命令已发送')
addLog(`${statusIcon('ok', 14)} Gateway 重启命令已发送`)
// 3. 等待 Gateway 启动
addLog('⏳ 等待 Gateway 启动8秒...')
addLog(`${icon('clock', 14)} 等待 Gateway 启动8秒...`)
await new Promise(resolve => setTimeout(resolve, 8000))
// 4. 检查 Gateway 状态
addLog('🔍 检查 Gateway 状态...')
addLog(`${icon('search', 14)} 检查 Gateway 状态...`)
const services = await api.getServicesStatus()
const running = services?.[0]?.running
if (running) {
addLog('✅ Gateway 已启动')
addLog(`${statusIcon('ok', 14)} Gateway 已启动`)
} else {
addLog('⚠️ Gateway 可能还在启动中,请稍后手动测试')
addLog(`${statusIcon('warn', 14)} Gateway 可能还在启动中,请稍后手动测试`)
}
// 5. 测试 WebSocket 连接
addLog('🔌 测试 WebSocket 连接...')
addLog(`${icon('plug', 14)} 测试 WebSocket 连接...`)
const config = await api.readOpenclawConfig()
const port = config?.gateway?.port || 18789
const token = config?.gateway?.auth?.token || ''
@@ -519,62 +522,63 @@ async function fixPairing(page) {
const ws = new WebSocket(url)
ws.onopen = () => {
addLog('✅ WebSocket 连接成功')
addLog(`${statusIcon('ok', 14)} WebSocket 连接成功`)
}
ws.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data)
if (msg.type === 'event' && msg.event === 'connect.challenge') {
addLog('✅ 收到 connect.challenge')
addLog(`${statusIcon('ok', 14)} 收到 connect.challenge`)
const nonce = msg.payload?.nonce || ''
api.createConnectFrame(nonce, token).then(frame => {
ws.send(JSON.stringify(frame))
addLog('📤 已发送 connect frame')
addLog(`${icon('send', 14)} 已发送 connect frame`)
})
}
if (msg.type === 'res' && msg.id?.startsWith('connect-')) {
if (msg.ok) {
addLog('🎉 握手成功!配对问题已修复!')
addLog('💡 正在重新建立主应用 WebSocket 连接...')
addLog(`${statusIcon('ok', 14)} 握手成功!配对问题已修复!`)
addLog(`${icon('lightbulb', 14)} 正在重新建立主应用 WebSocket 连接...`)
ws.close(1000)
// 触发主应用的 wsClient 重连,让主界面正常工作
wsClient.reconnect()
setTimeout(() => loadDebugInfo(page), 2000)
} else {
const errMsg = msg.error?.message || msg.error?.code || '未知错误'
addLog(` 握手失败: ${errMsg}`)
addLog(`${statusIcon('err', 14)} 握手失败: ${errMsg}`)
if (errMsg.includes('origin not allowed')) {
addLog('💡 原因Gateway 拒绝了当前应用的 origin需要重启 Gateway 再试')
addLog(`${icon('lightbulb', 14)} 原因Gateway 拒绝了当前应用的 origin需要重启 Gateway 再试`)
} else {
addLog('💡 建议:请手动前往“服务管理”页面重启 Gateway')
addLog(`${icon('lightbulb', 14)} 建议:请手动前往“服务管理”页面重启 Gateway`)
}
}
}
} catch (e) {
addLog(`⚠️ 解析消息失败: ${e}`)
addLog(`${statusIcon('warn', 14)} 解析消息失败: ${e}`)
}
}
ws.onerror = () => {
addLog('❌ WebSocket 连接失败,请确认 Gateway 已在运行')
addLog(`${statusIcon('err', 14)} WebSocket 连接失败,请确认 Gateway 已在运行`)
}
ws.onclose = (e) => {
if (e.code === 1008) {
addLog(`⚠️ 连接被拒绝 (1008) - Gateway 拒绝了当前 origin`)
addLog('💡 该问题应已被本次修复流程处理,请再次点击“一键修复配对”')
addLog(`${statusIcon('warn', 14)} 连接被拒绝 (1008) - Gateway 拒绝了当前 origin`)
addLog(`${icon('lightbulb', 14)} 该问题应已被本次修复流程处理,请再次点击“一键修复配对”`)
} else if (e.code !== 1000) {
addLog(`⚠️ 连接关闭 - Code: ${e.code}`)
addLog(`${statusIcon('warn', 14)} 连接关闭 - Code: ${e.code}`)
}
}
} catch (e) {
addLog(` 修复失败: ${e}`)
addLog('💡 建议:请手动前往"服务管理"页面重启 Gateway')
addLog(`${statusIcon('err', 14)} 修复失败: ${e}`)
addLog(`${icon('lightbulb', 14)} 建议:请手动前往"服务管理"页面重启 Gateway`)
} finally {
if (fixBtn) { fixBtn.disabled = false; fixBtn.textContent = '一键修复配对' }
}
}

View File

@@ -9,6 +9,7 @@ import { renderMarkdown } from '../lib/markdown.js'
import { saveMessage, saveMessages, getLocalMessages, isStorageAvailable } from '../lib/message-db.js'
import { toast } from '../components/toast.js'
import { showModal, showConfirm } from '../components/modal.js'
import { icon as svgIcon } from '../lib/icons.js'
const RENDER_THROTTLE = 30
const STORAGE_SESSION_KEY = 'clawpanel-last-session'
@@ -134,11 +135,41 @@ export async function render() {
bindEvents(page)
bindConnectOverlay(page)
// 首次使用引导提示
showPageGuide(_messagesEl)
// 非阻塞:先返回 DOM后台连接 Gateway
connectGateway()
return page
}
const GUIDE_KEY = 'clawpanel-guide-chat-dismissed'
function showPageGuide(container) {
if (localStorage.getItem(GUIDE_KEY)) return
const guide = document.createElement('div')
guide.className = 'chat-page-guide'
guide.innerHTML = `
<div class="chat-guide-inner">
<div class="chat-guide-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="28" height="28"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
</div>
<div class="chat-guide-content">
<b>你正在使用「实时聊天」</b>
<p>此页面通过 <b>Gateway</b> 连接 OpenClaw 的 AI Agent对话由你部署的 OpenClaw 服务处理。</p>
<p style="opacity:0.7;font-size:11px">如需使用 ClawPanel 内置 AI 助手(独立于 OpenClaw请前往左侧菜单「AI 助手」页面。</p>
</div>
<button class="chat-guide-close" title="知道了">&times;</button>
</div>
`
guide.querySelector('.chat-guide-close').onclick = () => {
localStorage.setItem(GUIDE_KEY, '1')
guide.remove()
}
container.insertBefore(guide, container.firstChild)
}
// ── 事件绑定 ──
function bindEvents(page) {
@@ -1101,7 +1132,7 @@ function appendUserMessage(text, attachments = [], msgTime) {
} else if (att.fileName || att.name) {
const card = document.createElement('div')
card.className = 'msg-file-card'
card.innerHTML = `<span class="msg-file-icon">📎</span><span class="msg-file-name">${att.fileName || att.name}</span>`
card.innerHTML = `<span class="msg-file-icon">${svgIcon('paperclip', 16)}</span><span class="msg-file-name">${att.fileName || att.name}</span>`
mediaContainer.appendChild(card)
}
})
@@ -1212,10 +1243,10 @@ function appendFilesToEl(el, files) {
const card = document.createElement('div')
card.className = 'msg-file-card'
const ext = (f.name || '').split('.').pop().toLowerCase()
const iconMap = { pdf: '📄', doc: '📝', docx: '📝', txt: '📃', md: '📃', json: '📋', csv: '📊', zip: '📦', rar: '📦' }
const icon = iconMap[ext] || '📎'
const fileIconMap = { pdf: 'file', doc: 'file-text', docx: 'file-text', txt: 'file-plain', md: 'file-plain', json: 'clipboard', csv: 'bar-chart', zip: 'package', rar: 'package' }
const fileIcon = svgIcon(fileIconMap[ext] || 'paperclip', 16)
const size = f.size ? formatFileSize(f.size) : ''
card.innerHTML = `<span class="msg-file-icon">${icon}</span><div class="msg-file-info"><span class="msg-file-name">${f.name || '文件'}</span>${size ? `<span class="msg-file-size">${size}</span>` : ''}</div>`
card.innerHTML = `<span class="msg-file-icon">${fileIcon}</span><div class="msg-file-info"><span class="msg-file-name">${f.name || '文件'}</span>${size ? `<span class="msg-file-size">${size}</span>` : ''}</div>`
if (f.url) {
card.style.cursor = 'pointer'
card.onclick = () => window.open(f.url, '_blank')

View File

@@ -4,6 +4,7 @@
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { onGatewayChange } from '../lib/app-state.js'
import { navigate } from '../router.js'
let _unsubGw = null
@@ -36,6 +37,9 @@ export async function render() {
</div>
`
// 绑定事件(只绑一次)
bindActions(page)
// 异步加载数据
loadDashboardData(page)
@@ -94,7 +98,6 @@ async function loadDashboardData(page) {
}
renderStatCards(page, services, version, [], config, null)
bindActions(page)
// 第二波Agent、隧道、MCP、ClawApp、备份 → 更新卡片 + 渲染总览
const [agentsRes, tunnelRes, mcpRes, clawappRes, backupsRes] = await secondaryP
@@ -195,8 +198,14 @@ function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, con
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
Gateway 核心网关
</div>
<div class="overview-value" style="color: ${gw?.running ? 'var(--success)' : 'var(--error)'}">
${gw?.running ? '运行中' : '已停止'}
<div class="overview-actions">
<span class="overview-status" style="color: ${gw?.running ? 'var(--success)' : 'var(--error)'}">
${gw?.running ? '运行中' : '已停止'}
</span>
${gw?.running
? '<button class="btn btn-danger btn-xs" data-action="stop-gw">停止</button><button class="btn btn-secondary btn-xs" data-action="restart-gw">重启</button>'
: '<button class="btn btn-primary btn-xs" data-action="start-gw">启动</button>'
}
</div>
</div>
<div class="overview-item">
@@ -204,8 +213,16 @@ function renderOverview(page, services, clawapp, tunnel, mcpConfig, backups, con
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
ClawApp 守护进程
</div>
<div class="overview-value" style="color: ${clawapp?.running ? 'var(--success)' : 'var(--error)'}">
${clawapp?.running ? '端口 ' + clawapp.port : '未启动'}
<div class="overview-actions">
<span class="overview-status" style="color: ${clawapp?.running ? 'var(--success)' : 'var(--error)'}">
${clawapp?.running ? '端口 ' + clawapp.port : '未启动'}
</span>
${clawapp?.installed
? (clawapp?.running
? `<a class="btn btn-primary btn-xs" href="${clawapp.url || 'http://localhost:3210'}" target="_blank" rel="noopener">打开</a>`
: '<button class="btn btn-secondary btn-xs" data-action="goto-extensions">前往管理</button>')
: '<button class="btn btn-secondary btn-xs" data-action="goto-extensions">去安装</button>'
}
</div>
</div>
<div class="overview-item">
@@ -286,6 +303,44 @@ function bindActions(page) {
const btnUpdate = page.querySelector('#btn-check-update')
const btnCreateBackup = page.querySelector('#btn-create-backup')
// 概览区域的 Gateway 启动/停止/重启 + ClawApp 导航
page.addEventListener('click', async (e) => {
const actionBtn = e.target.closest('[data-action]')
if (!actionBtn) return
const action = actionBtn.dataset.action
if (action === 'start-gw') {
actionBtn.disabled = true; actionBtn.textContent = '启动中...'
try {
await api.startService('ai.openclaw.gateway')
toast('Gateway 启动指令已发送', 'success')
setTimeout(() => loadDashboardData(page), 2000)
} catch (err) { toast('启动失败: ' + err, 'error') }
finally { actionBtn.disabled = false; actionBtn.textContent = '启动' }
}
if (action === 'stop-gw') {
actionBtn.disabled = true; actionBtn.textContent = '停止中...'
try {
await api.stopService('ai.openclaw.gateway')
toast('Gateway 已停止', 'success')
setTimeout(() => loadDashboardData(page), 1500)
} catch (err) { toast('停止失败: ' + err, 'error') }
finally { actionBtn.disabled = false; actionBtn.textContent = '停止' }
}
if (action === 'restart-gw') {
actionBtn.disabled = true; actionBtn.textContent = '重启中...'
try {
await api.restartService('ai.openclaw.gateway')
toast('Gateway 重启指令已发送', 'success')
setTimeout(() => loadDashboardData(page), 3000)
} catch (err) { toast('重启失败: ' + err, 'error') }
finally { actionBtn.disabled = false; actionBtn.textContent = '重启' }
}
if (action === 'goto-extensions') {
navigate('/extensions')
}
})
btnRestart?.addEventListener('click', async () => {
btnRestart.disabled = true
btnRestart.classList.add('btn-loading')

View File

@@ -4,6 +4,7 @@
*/
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { statusIcon } from '../lib/icons.js'
// HTML 转义,防止 XSS
function escapeHtml(str) {
@@ -306,16 +307,24 @@ async function handleInstallCftunnel(page) {
await api.installCftunnel()
progressFill.classList.add('done')
progressText.textContent = '✅ 安装完成'
progressText.innerHTML = `${statusIcon('ok', 14)} 安装完成`
toast('cftunnel 安装成功', 'success')
// 3 秒后刷新状态
setTimeout(() => loadCftunnel(page), 3000)
} catch (e) {
progressFill.classList.add('error')
progressText.textContent = '❌ 安装失败'
progressText.innerHTML = `${statusIcon('err', 14)} 安装失败`
logBox.textContent += '\n错误: ' + e
toast('安装失败: ' + e, 'error')
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: '安装 cftunnel 失败',
error: logBox.textContent,
scene: '安装 cftunnel 内网穿透工具',
hint: String(e),
})
}
} finally {
unlistenLog?.()
unlistenProgress?.()
@@ -364,15 +373,23 @@ async function handleInstallClawapp(page) {
await api.installClawapp()
progressFill.classList.add('done')
progressText.textContent = '✅ 安装完成'
progressText.innerHTML = `${statusIcon('ok', 14)} 安装完成`
toast('ClawApp 安装成功', 'success')
setTimeout(() => loadClawapp(page), 3000)
} catch (e) {
progressFill.classList.add('error')
progressText.textContent = '❌ 安装失败'
progressText.innerHTML = `${statusIcon('err', 14)} 安装失败`
logBox.textContent += '\n错误: ' + e
toast('安装失败: ' + e, 'error')
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: '安装 ClawApp 失败',
error: logBox.textContent,
scene: '安装 ClawApp 手机客户端',
hint: String(e),
})
}
} finally {
unlistenLog?.()
unlistenProgress?.()

View File

@@ -5,6 +5,7 @@
import { api } from '../lib/tauri-api.js'
import { toast } from '../components/toast.js'
import { showModal, showConfirm } from '../components/modal.js'
import { icon, statusIcon } from '../lib/icons.js'
// API 接口类型选项
const API_TYPES = [
@@ -22,6 +23,28 @@ const PROVIDER_PRESETS = [
{ key: 'google', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', api: 'google-gemini' },
]
// gpt.qt.cool 推广配置
const QTCOOL = {
baseUrl: 'https://gpt.qt.cool/v1',
defaultKey: 'sk-0JDu7hyc51ZKD4iNebpFu07EUEhXmVVc',
site: 'https://gpt.qt.cool/',
usageUrl: 'https://gpt.qt.cool/user?key=',
providerKey: 'qtcool',
api: 'openai-completions',
models: [
{ id: 'gpt-5.4', name: 'GPT-5.4', contextWindow: 128000 },
{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.2', name: 'GPT-5.2', contextWindow: 128000 },
{ id: 'gpt-5.1-codex-max', name: 'GPT-5.1 Codex Max', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.1-codex-mini', name: 'GPT-5.1 Codex Mini', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5.1', name: 'GPT-5.1', contextWindow: 128000 },
{ id: 'gpt-5-codex', name: 'GPT-5 Codex', contextWindow: 128000, reasoning: true },
{ id: 'gpt-5', name: 'GPT-5', contextWindow: 128000 },
]
}
// 常用模型预设(按服务商分组)
const MODEL_PRESETS = {
openai: [
@@ -60,6 +83,30 @@ export async function render() {
服务商是模型的来源(如 OpenAI、DeepSeek 等)。每个服务商下可添加多个模型。
标记为「主模型」的将优先使用,其余作为备选自动切换。配置修改后自动保存。
</div>
<div id="qtcool-promo" style="margin-bottom:var(--space-lg);border-radius:12px;background:linear-gradient(135deg,#0f0c29 0%,#302b63 50%,#24243e 100%);color:#fff;position:relative;overflow:hidden;box-shadow:0 4px 24px rgba(48,43,99,0.25)">
<div style="position:absolute;top:-50px;right:-50px;width:200px;height:200px;border-radius:50%;background:radial-gradient(circle,rgba(99,102,241,0.12) 0%,transparent 70%);pointer-events:none"></div>
<div style="position:absolute;bottom:-30px;left:20px;width:120px;height:120px;border-radius:50%;background:radial-gradient(circle,rgba(168,85,247,0.08) 0%,transparent 70%);pointer-events:none"></div>
<div style="padding:20px 24px 16px;display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:16px">
<div style="flex:1;min-width:240px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<span style="font-size:20px">${icon('gift', 22)}</span>
<span style="font-weight:700;font-size:16px;letter-spacing:0.3px">ClawPanel 公益 AI 接口计划</span>
</div>
<div style="font-size:13px;color:rgba(255,255,255,0.65);line-height:1.7">
Token 费用我们帮你出了。调用成本由项目组内部承担GPT-5 全系列模型开箱即用。<br>
无需注册、无需付费、支持 OpenAI 兼容接口 — 点击即享。
</div>
</div>
<div style="display:flex;flex-direction:column;gap:10px;align-items:flex-end">
<button class="btn btn-sm" id="btn-qtcool-oneclick" style="background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;font-weight:600;border:none;padding:8px 22px;font-size:13px;white-space:nowrap;border-radius:8px;box-shadow:0 2px 12px rgba(99,102,241,0.4);cursor:pointer;transition:transform 0.15s">${icon('zap', 14)} 一键添加全部模型</button>
<div style="display:flex;gap:14px;font-size:11px">
<a href="https://gpt.qt.cool/checkin" target="_blank" style="color:rgba(168,133,247,0.9);text-decoration:none">${icon('target', 12)} 签到领密钥</a>
<a href="${QTCOOL.usageUrl}${QTCOOL.defaultKey}" target="_blank" style="color:rgba(168,133,247,0.9);text-decoration:none">${icon('bar-chart', 12)} 用量查询</a>
<a href="https://claw.qt.cool/" target="_blank" style="color:rgba(168,133,247,0.9);text-decoration:none">${icon('home', 12)} 官网</a>
</div>
</div>
</div>
</div>
<div id="default-model-bar"></div>
<div style="margin-bottom:var(--space-md)">
<input class="form-input" id="model-search" placeholder="搜索模型(按 ID 或名称过滤)" style="max-width:360px">
@@ -695,6 +742,71 @@ function applyDefaultModel(state) {
function bindTopActions(page, state) {
page.querySelector('#btn-add-provider').onclick = () => addProvider(page, state)
page.querySelector('#btn-undo').onclick = () => undo(page, state)
// gpt.qt.cool 一键添加(动态获取模型列表)
page.querySelector('#btn-qtcool-oneclick').onclick = async () => {
if (!state.config) { toast('配置未加载完成,请稍候', 'warning'); return }
const btn = page.querySelector('#btn-qtcool-oneclick')
btn.textContent = '获取模型列表...'
btn.disabled = true
// 动态获取模型列表,失败则用静态 fallback
let models = QTCOOL.models
try {
const resp = await fetch(QTCOOL.baseUrl + '/models', {
headers: { 'Authorization': 'Bearer ' + QTCOOL.defaultKey },
signal: AbortSignal.timeout(8000)
})
if (resp.ok) {
const data = await resp.json()
if (data.data && data.data.length) {
models = data.data.map(m => ({
id: m.id, name: m.id, contextWindow: 128000,
reasoning: m.id.includes('codex')
})).sort((a, b) => b.id.localeCompare(a.id))
}
}
} catch { /* use fallback */ }
btn.innerHTML = `${icon('zap', 14)} 一键添加全部模型`
btn.disabled = false
pushUndo(state)
if (!state.config.models) state.config.models = {}
if (!state.config.models.providers) state.config.models.providers = {}
const existing = state.config.models.providers[QTCOOL.providerKey]
if (existing) {
const existingIds = new Set((existing.models || []).map(m => typeof m === 'string' ? m : m.id))
let added = 0
for (const m of models) {
if (!existingIds.has(m.id)) {
existing.models.push({ ...m })
added++
}
}
toast(added ? `已添加 ${added} 个新模型到 qtcool` : 'qtcool 模型已是最新', added ? 'success' : 'info')
} else {
state.config.models.providers[QTCOOL.providerKey] = {
baseUrl: QTCOOL.baseUrl,
apiKey: QTCOOL.defaultKey,
api: QTCOOL.api,
models: models.map(m => ({ ...m })),
}
if (!getCurrentPrimary(state.config)) {
if (!state.config.agents) state.config.agents = {}
if (!state.config.agents.defaults) state.config.agents.defaults = {}
if (!state.config.agents.defaults.model) state.config.agents.defaults.model = {}
state.config.agents.defaults.model.primary = QTCOOL.providerKey + '/' + models[0].id
}
toast('已添加 gpt.qt.cool' + models.length + ' 个模型)', 'success')
}
renderProviders(page, state)
renderDefaultBar(page, state)
updateUndoBtn(page, state)
autoSave(state)
}
}
// 添加服务商(带预设快捷选择)
@@ -1073,7 +1185,7 @@ async function handleBatchTest(section, state, providerKey) {
renderDefaultBar(page, state)
}
// 进度 toast
const status = model?.testStatus === 'ok' ? '' : ''
const status = model?.testStatus === 'ok' ? '\u2713' : '\u2717'
const latStr = model?.latency != null ? ` ${(model.latency / 1000).toFixed(1)}s` : ''
toast(`${status} ${modelId}${latStr} (${ok + fail}/${ids.length})`, model?.testStatus === 'ok' ? 'success' : 'error')
}

View File

@@ -3,6 +3,7 @@
* 支持 Web 部署模式和 Tauri 桌面端
*/
import { toast } from '../components/toast.js'
import { statusIcon } from '../lib/icons.js'
const isTauri = !!window.__TAURI_INTERNALS__
let _tauriApi = null
@@ -114,7 +115,7 @@ function renderContent(container, status) {
let html = ''
// 当前状态
const stateIcon = status.hasPassword ? '✅' : (status.ignoreRisk ? '⚠️' : '⚠️')
const stateIcon = status.hasPassword ? statusIcon('ok', 20) : statusIcon('warn', 20)
const stateText = status.hasPassword
? (status.mustChangePassword ? '使用默认密码(需修改)' : '已设置自定义密码')
: (status.ignoreRisk ? '无视风险模式(无密码)' : '未设置密码')

View File

@@ -7,6 +7,7 @@ import { toast } from '../components/toast.js'
import { showConfirm, showUpgradeModal } from '../components/modal.js'
import { isMacPlatform, setUpgrading, setUserStopped, resetAutoRestart } from '../lib/app-state.js'
import { diagnoseInstallError } from '../lib/error-diagnosis.js'
import { icon, statusIcon } from '../lib/icons.js'
// HTML 转义,防止 XSS
function escapeHtml(str) {
@@ -424,11 +425,20 @@ async function doUpgradeWithModal(source, page) {
} catch (e) {
const errStr = String(e)
modal.appendLog(errStr)
const diagnosis = diagnoseInstallError(errStr)
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendLog(' ' + diagnosis.hint)
if (diagnosis.command) modal.appendLog('💻 ' + diagnosis.command)
if (diagnosis.hint) modal.appendHtmlLog(`${statusIcon('info', 14)} ${diagnosis.hint}`)
if (diagnosis.command) modal.appendHtmlLog(`${icon('clipboard', 14)} ${diagnosis.command}`)
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: diagnosis.title,
error: fullLog,
scene: '升级 OpenClaw',
hint: diagnosis.hint,
})
}
} finally {
setUpgrading(false)
unlistenLog?.()

View File

@@ -7,6 +7,7 @@ import { showUpgradeModal } from '../components/modal.js'
import { toast } from '../components/toast.js'
import { setUpgrading, isMacPlatform } from '../lib/app-state.js'
import { diagnoseInstallError } from '../lib/error-diagnosis.js'
import { icon, statusIcon } from '../lib/icons.js'
export async function render() {
const page = document.createElement('div')
@@ -107,7 +108,7 @@ function renderSteps(page, { node, cliOk, config }) {
: `安装 Node.js 后需要<strong>重启 ClawPanel</strong>,新的环境变量才能生效。`
}
<div style="margin-top:8px;display:flex;gap:6px;align-items:center;flex-wrap:wrap">
<button class="btn btn-secondary btn-sm" id="btn-scan-node" style="font-size:11px;padding:3px 10px">🔍 自动扫描</button>
<button class="btn btn-secondary btn-sm" id="btn-scan-node" style="font-size:11px;padding:3px 10px">${icon('search', 12)} 自动扫描</button>
<span style="color:var(--text-tertiary)">或手动指定路径:</span>
</div>
<div style="margin-top:6px;display:flex;gap:6px">
@@ -218,7 +219,7 @@ function renderInstallSection() {
<div style="margin-bottom:10px">
<div style="font-weight:600;margin-bottom:4px">WSL 中使用 Web 版:</div>
<div style="margin-bottom:2px;opacity:0.8">打开 WSL 终端,一键部署 ClawPanel Web 版:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://claw.qt.cool/deploy.sh | bash</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
<div style="margin-top:4px;opacity:0.7">部署后在浏览器访问 WSL 的 IP 即可管理。</div>
</div>
` : ''}
@@ -226,12 +227,12 @@ function renderInstallSection() {
<div style="font-weight:600;margin-bottom:4px">Docker 容器中使用:</div>
<div style="margin-bottom:2px;opacity:0.8">在容器内安装 OpenClaw + ClawPanel Web 版:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all;margin-bottom:4px">npm i -g @qingchencloud/openclaw-zh</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://claw.qt.cool/deploy.sh | bash</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
</div>
<div>
<div style="font-weight:600;margin-bottom:4px">远程服务器:</div>
<div style="margin-bottom:2px;opacity:0.8">SSH 登录服务器后执行:</div>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://claw.qt.cool/deploy.sh | bash</code>
<code style="display:block;background:var(--bg-secondary);padding:6px 10px;border-radius:4px;user-select:all;word-break:break-all">curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawpanel/main/deploy.sh | bash</code>
</div>
</div>
</details>
@@ -363,7 +364,7 @@ function bindEvents(page, nodeOk, detectState) {
resultEl.innerHTML = `<span style="color:var(--danger)">扫描失败: ${e}</span>`
} finally {
btn.disabled = false
btn.textContent = '🔍 自动扫描'
btn.innerHTML = `${icon('search', 12)} 自动扫描`
}
})
@@ -425,9 +426,9 @@ function bindEvents(page, nodeOk, detectState) {
modal.appendLog('正在安装 Gateway 服务...')
try {
await api.installGateway()
modal.appendLog('✅ Gateway 服务已安装')
modal.appendHtmlLog(`${statusIcon('ok', 14)} Gateway 服务已安装`)
} catch (e) {
modal.appendLog('⚠️ Gateway 安装失败: ' + e)
modal.appendHtmlLog(`${statusIcon('warn', 14)} Gateway 安装失败: ${e}`)
}
// 确保 openclaw.json 有关键默认值,否则 Gateway 启动不了或功能受限
@@ -439,7 +440,7 @@ function bindEvents(page, nodeOk, detectState) {
if (!config.gateway.mode) {
config.gateway.mode = 'local'
patched = true
modal.appendLog('✅ 已设置 Gateway 运行模式为 local')
modal.appendHtmlLog(`${statusIcon('ok', 14)} 已设置 Gateway 运行模式为 local`)
}
if (!config.tools || config.tools.profile !== 'full') {
config.tools = { profile: 'full', sessions: { visibility: 'all' }, ...(config.tools || {}) }
@@ -447,12 +448,12 @@ function bindEvents(page, nodeOk, detectState) {
if (!config.tools.sessions) config.tools.sessions = {}
config.tools.sessions.visibility = 'all'
patched = true
modal.appendLog('✅ 已开启 Agent 工具全部权限')
modal.appendHtmlLog(`${statusIcon('ok', 14)} 已开启 Agent 工具全部权限`)
}
if (patched) await api.writeOpenclawConfig(config)
}
} catch (e) {
modal.appendLog('⚠️ 自动配置失败: ' + e)
modal.appendHtmlLog(`${statusIcon('warn', 14)} 自动配置失败: ${e}`)
}
toast('OpenClaw 安装成功', 'success')
@@ -460,11 +461,23 @@ function bindEvents(page, nodeOk, detectState) {
} catch (e) {
const errStr = String(e)
modal.appendLog(errStr)
const diagnosis = diagnoseInstallError(errStr)
// 等待 Tauri 事件队列中残留的 npm 日志行被 JS 处理完毕,
// 确保 getLogText() 包含完整输出(含 exit code / ENOENT 等关键行)
await new Promise(r => setTimeout(r, 150))
const fullLog = modal.getLogText() + '\n' + errStr
const diagnosis = diagnoseInstallError(fullLog)
modal.setError(diagnosis.title)
if (diagnosis.hint) modal.appendLog('')
if (diagnosis.hint) modal.appendLog(' ' + diagnosis.hint)
if (diagnosis.command) modal.appendLog('💻 ' + diagnosis.command)
if (diagnosis.hint) modal.appendHtmlLog(`${statusIcon('info', 14)} ${diagnosis.hint}`)
if (diagnosis.command) modal.appendHtmlLog(`${icon('clipboard', 14)} ${diagnosis.command}`)
if (window.__openAIDrawerWithError) {
window.__openAIDrawerWithError({
title: diagnosis.title,
error: fullLog,
scene: '初始安装 OpenClaw',
hint: diagnosis.hint,
})
}
} finally {
setUpgrading(false)
unlistenLog?.()

156
src/style/ai-drawer.css Normal file
View File

@@ -0,0 +1,156 @@
/* ── 全局 AI 助手浮动按钮 ── */
.ai-fab {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 9000;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--accent);
color: #fff;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.35);
transition: transform 0.2s, box-shadow 0.2s;
touch-action: none; /* 拖动时禁止浏览器默认手势 */
user-select: none;
}
.ai-fab:hover {
transform: scale(1.08);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.45);
}
.ai-fab.dragging {
opacity: 0.85;
transform: scale(1.12);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.5);
cursor: grabbing;
}
.ai-fab svg {
width: 22px;
height: 22px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
pointer-events: none; /* 防止 SVG 吃掉 pointer 事件 */
}
/* FAB 红点:有待处理的错误上下文 */
.ai-fab.has-error::after {
content: '';
position: absolute;
top: 2px;
right: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--error, #ef4444);
border: 2px solid #fff;
animation: fab-pulse 1.5s ease-in-out infinite;
}
@keyframes fab-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.2); opacity: 0.8; }
}
/* ── 错误上下文 Banner助手页面内 ── */
.ast-error-banner {
margin: 0 0 12px;
padding: 10px 14px;
background: var(--error-muted, rgba(239, 68, 68, 0.08));
border: 1px solid var(--error-border, rgba(239, 68, 68, 0.25));
border-radius: var(--radius-lg, 12px);
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
position: relative;
}
.ast-error-banner-header {
display: flex;
align-items: center;
gap: 8px;
}
.ast-error-banner-icon {
flex-shrink: 0;
color: var(--error, #ef4444);
font-size: 16px;
}
.ast-error-banner-title {
flex: 1;
font-weight: 600;
color: var(--text-primary);
font-size: 13px;
}
.ast-error-banner-actions {
display: flex;
gap: 6px;
flex-shrink: 0;
}
.ast-error-banner-actions button {
padding: 3px 10px;
border-radius: 6px;
border: 1px solid transparent;
font-size: 11px;
cursor: pointer;
transition: background 0.15s;
}
.ast-error-banner .btn-analyze {
background: var(--error, #ef4444);
color: #fff;
border-color: var(--error, #ef4444);
}
.ast-error-banner .btn-analyze:hover {
opacity: 0.9;
}
.ast-error-banner .btn-dismiss {
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.ast-error-banner .btn-dismiss:hover {
background: var(--bg-secondary);
}
.ast-error-banner-hint {
margin-top: 6px;
color: var(--text-tertiary);
}
.ast-error-banner-detail {
margin-top: 8px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.ast-error-banner-detail.expanded {
max-height: 300px;
}
.ast-error-banner-detail pre {
margin: 0;
padding: 8px 10px;
background: var(--bg-tertiary);
border-radius: 6px;
font-size: 11px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
max-height: 280px;
overflow-y: auto;
color: var(--text-secondary);
}
.ast-error-toggle {
background: none;
border: none;
color: var(--text-tertiary);
font-size: 11px;
cursor: pointer;
padding: 0;
margin-top: 4px;
text-decoration: underline;
}
.ast-error-toggle:hover {
color: var(--text-primary);
}

View File

@@ -426,6 +426,57 @@
border-color: var(--accent);
}
/* 首次引导提示 */
.ast-page-guide {
display: flex;
align-items: center;
gap: 10px;
margin-top: 16px;
padding: 10px 14px;
background: var(--accent-muted, rgba(99, 102, 241, 0.08));
border: 1px solid var(--accent-border, rgba(99, 102, 241, 0.2));
border-radius: var(--radius-lg);
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
text-align: left;
width: 100%;
max-width: 560px;
position: relative;
}
.ast-guide-badge {
flex-shrink: 0;
background: var(--accent);
color: #fff;
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
white-space: nowrap;
}
.ast-guide-text {
flex: 1;
min-width: 0;
}
.ast-guide-text b {
color: var(--text-primary);
}
.ast-guide-close {
flex-shrink: 0;
background: none;
border: none;
color: var(--text-tertiary);
font-size: 18px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.ast-guide-close:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
/* ── Skills 网格 ── */
.ast-skills-grid {
display: grid;
@@ -1520,3 +1571,112 @@
justify-content: flex-end;
gap: 8px;
}
/* ── 灵魂移植卡片 ── */
.ast-soul-card {
border: 1px solid var(--border-secondary);
border-radius: var(--radius-md);
background: var(--bg-primary);
overflow: hidden;
}
.ast-soul-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-secondary);
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
}
.ast-soul-header svg { flex-shrink: 0; }
.ast-soul-files {
display: flex;
flex-direction: column;
}
.ast-soul-file {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 14px;
font-size: 12px;
border-bottom: 1px solid var(--border-secondary);
transition: background var(--transition-fast);
}
.ast-soul-file:last-child { border-bottom: none; }
.ast-soul-file:hover { background: var(--bg-card-hover); }
.ast-soul-file-icon {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.ast-soul-file.loaded .ast-soul-file-icon {
background: var(--success-muted, rgba(34,197,94,0.1));
color: var(--success);
}
.ast-soul-file.missing .ast-soul-file-icon {
background: var(--error-muted, rgba(239,68,68,0.1));
color: var(--error);
}
.ast-soul-file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
}
.ast-soul-file-name {
font-weight: 600;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 12px;
}
.ast-soul-file.missing .ast-soul-file-name {
color: var(--text-tertiary);
}
.ast-soul-file-desc {
font-size: 11px;
color: var(--text-tertiary);
line-height: 1.2;
}
.ast-soul-file-size {
font-size: 11px;
color: var(--text-tertiary);
font-family: var(--font-mono);
flex-shrink: 0;
}
/* 按钮样式补充 */
.btn-ghost {
background: transparent;
border: 1px solid var(--border-secondary);
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--bg-card-hover);
border-color: var(--border-primary);
color: var(--text-primary);
}
.btn svg { vertical-align: -2px; }
/* 加载旋转动画 */
.ast-spin {
animation: ast-spin 1s linear infinite;
}
@keyframes ast-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}

View File

@@ -783,6 +783,54 @@
color: var(--text-tertiary);
}
/* 首次引导提示 */
.chat-page-guide {
margin: 0 16px 8px;
flex-shrink: 0;
}
.chat-guide-inner {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 14px;
background: var(--accent-muted, rgba(99, 102, 241, 0.08));
border: 1px solid var(--accent-border, rgba(99, 102, 241, 0.2));
border-radius: var(--radius-lg);
font-size: 12px;
line-height: 1.6;
color: var(--text-secondary);
position: relative;
}
.chat-guide-icon {
flex-shrink: 0;
color: var(--accent);
margin-top: 2px;
}
.chat-guide-content b {
color: var(--text-primary);
font-size: 13px;
}
.chat-guide-content p {
margin: 4px 0 0;
}
.chat-guide-close {
position: absolute;
top: 6px;
right: 8px;
background: none;
border: none;
color: var(--text-tertiary);
font-size: 18px;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
line-height: 1;
}
.chat-guide-close:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.chat-lightbox-img {
max-width: 90%;
max-height: 90%;

View File

@@ -140,6 +140,12 @@
font-size: var(--font-size-xs);
}
.btn-xs {
padding: 2px 8px;
font-size: 11px;
border-radius: var(--radius-sm);
}
/* 表单 */
.form-group {
margin-bottom: var(--space-lg);
@@ -275,8 +281,16 @@ mark {
padding: var(--space-xl);
min-width: 360px;
max-width: 500px;
max-height: calc(100vh - 60px);
display: flex;
flex-direction: column;
box-shadow: var(--shadow-lg);
}
.modal-body {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.modal-title {
font-size: var(--font-size-lg);

View File

@@ -50,6 +50,17 @@
color: var(--text-tertiary);
}
.overview-actions {
display: flex;
align-items: center;
gap: 8px;
}
.overview-status {
font-size: var(--font-size-sm);
font-weight: 600;
margin-right: 4px;
}
.overview-value {
display: flex;
align-items: center;