feat: 新增 AI 助手页面 + 图片识别功能 + 更新宣传素材

- 新增 AI 助手页面 (assistant.js/assistant.css/assistant.rs)
- 新增图片识别截图 (13.png) 并加入官网和 README
- 更新宣传视频 (promo-web.mp4) 含 AI 助手+工具调用+图片识别场景
- 更新视频封面 (video-cover.png/video-cover-light.png) 突出 AI 助手
- 更新 AI 助手演示 GIF (ai-assistant-demo.gif) 作为 README 首图
- 更新功能矩阵 GIF (feature-showcase.gif) AI 助手为 star 卡片
- 官网新增图片识别 showcase 区块
- README 新增图片识别特性和截图
- 视频封面改用专业设计版本
- 演示视频时长 badge 更新为 50 秒
This commit is contained in:
晴天
2026-03-06 04:33:43 +08:00
parent 7eb78ea186
commit 860218fa09
26 changed files with 5046 additions and 50 deletions

View File

@@ -3,7 +3,7 @@
</p>
<p align="center">
OpenClaw 可视化管理面板 — 基于 Tauri v2 的跨平台桌面应用
内置 AI 助手的 OpenClaw 管理面板 — 一键安装、配置、诊断、修复
</p>
<p align="center">
@@ -24,16 +24,16 @@
---
<p align="center">
<img src="docs/terminal-demo.gif" width="800" alt="ClawPanel 安装演示">
<img src="docs/ai-assistant-demo.gif" width="800" alt="ClawPanel AI 助手演示">
</p>
<p align="center">
<a href="https://claw.qt.cool/#video">
<img src="https://img.shields.io/badge/%E2%96%B6%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91-38%E7%A7%92%E5%BF%AB%E9%80%9F%E4%BA%86%E8%A7%A3-6366f1?style=for-the-badge" alt="演示视频">
<img src="https://img.shields.io/badge/%E2%96%B6%20%E6%BC%94%E7%A4%BA%E8%A7%86%E9%A2%91-50%E7%A7%92%E5%BF%AB%E9%80%9F%E4%BA%86%E8%A7%A3-6366f1?style=for-the-badge" alt="演示视频">
</a>
</p>
ClawPanel 是 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) AI Agent 框架的可视化管理面板,提供服务管控、模型配置、日志查看、记忆管理等核心功能,一站式管理你的 OpenClaw 实例
ClawPanel 是 [OpenClaw](https://github.com/1186258278/OpenClawChineseTranslation) AI Agent 框架的可视化管理面板。**内置智能 AI 助手**,帮你一键安装 OpenClaw、自动诊断配置、排查问题、修复错误。8 大工具 + 4 种模式 + 交互式问答,从新手到老手都能轻松管理
> 🌐 **官网**: [claw.qt.cool](https://claw.qt.cool/)  |  📦 **下载**: [GitHub Releases](https://github.com/qingchencloud/clawpanel/releases/latest)
@@ -107,6 +107,8 @@ docker run -d --name clawpanel --restart unless-stopped \
<img src="docs/feature-showcase.gif" width="800" alt="功能矩阵">
</p>
- **🤖 AI 助手(全新·重磅)** — 内置独立 AI 助手4 种操作模式 + 8 大工具 + 交互式问答,详见下方 [AI 助手亮点](#-ai-助手亮点)
- **🖼️ 图片识别** — 粘贴截图或拖拽图片AI 自动识别分析,支持多模态图文混排对话
- **仪表盘** — 系统概览,服务状态实时监控,快捷操作
- **服务管理** — OpenClaw 启停控制、版本检测与一键升级、Gateway 安装/卸载、配置备份与还原
- **模型配置** — 多服务商管理、模型增删改查、批量连通性测试、延迟检测、拖拽排序、自动保存+撤销
@@ -124,6 +126,26 @@ docker run -d --name clawpanel --restart unless-stopped \
<img src="docs/quick-stats.gif" width="800" alt="ClawPanel 数据概览">
</p>
<p align="center">
<img src="docs/00.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 助手工具调用实战">
</p>
<p align="center"><em>🔧 AI 实战 — 自动调用工具:获取系统信息 → 列出目录 → 读取配置 → 生成健康检查报告,全程可视化</em></p>
<p align="center">
<img src="docs/12.png" width="800" alt="AI 助手设置">
</p>
<p align="center"><em>⚙️ AI 设置 — 独立模型配置,支持任意 OpenAI 兼容 API无需安装 OpenClaw 也能使用 AI 助手</em></p>
<p align="center">
<img src="docs/13.png" width="800" alt="AI 图片识别">
</p>
<p align="center"><em>🖼️ 图片识别 — 粘贴截图或拖拽图片AI 自动识别分析内容,多模态图文混排对话</em></p>
<p align="center">
<img src="docs/01.png" width="800" alt="仪表盘">
</p>
@@ -179,6 +201,68 @@ docker run -d --name clawpanel --restart unless-stopped \
</details>
## 🤖 AI 助手亮点
ClawPanel 内置的 AI 助手不只是聊天机器人——它能**直接操作你的系统**,帮你诊断、修复、甚至提交 PR。
### 四种操作模式
一键切换,界面颜色随模式变化,清晰感知当前权限状态:
| 模式 | 图标 | 工具 | 写文件 | 确认 | 适用场景 |
|------|------|------|--------|------|---------|
| **聊天** | 💬 | ❌ | ❌ | — | 纯问答,不触碰系统 |
| **规划** | 📋 | ✅ | ❌ | ✅ | 读配置/查日志,输出方案不动文件 |
| **执行** | ⚡ | ✅ | ✅ | ✅ | 正常干活,危险操作弹确认 |
| **无限** | ∞ | ✅ | ✅ | ❌ | 全自动,工具调用不弹窗 |
设置中还有**工具开关**(终端/文件),优先级高于模式——关掉终端,即使无限模式也调不了命令。
### 八大工具
| 工具 | 功能 | 示例 |
|------|------|------|
| `ask_user` | 向用户提问(单选/多选/文本) | "选择要提交到哪个仓库?" |
| `get_system_info` | 获取 OS、架构、主目录 | 自动判断该用 PowerShell 还是 Bash |
| `run_command` | 执行 Shell 命令 | 重启 Gateway、查看日志 |
| `read_file` | 读取文件 | 读取 openclaw.json 分析配置 |
| `write_file` | 写入文件 | 修复配置错误、生成脚本 |
| `list_directory` | 浏览目录 | 列出 .openclaw/ 结构 |
| `list_processes` | 查看进程 | 检查 Gateway 是否在运行 |
| `check_port` | 检测端口占用 | 18789 端口被谁占了? |
### 交互式问答ask_user
AI 可以通过 `ask_user` 工具向你提问,支持三种交互方式:
- **单选** — 从多个方案中选一个,还能输入自定义答案
- **多选** — 勾选多项,比如"选择要检查的组件"
- **文本** — 自由输入,比如"描述你遇到的问题"
AI 等你回答后才会继续操作,实现真正的**人机协作**。
### PR 助手 & Bug 报告
发现 BugAI 不只是告诉你怎么修——它**直接帮你修**
1. 🐛 **提交 Bug 报告** — AI 自动收集系统环境、读取错误日志,按标准模板整理成 GitHub Issue你复制粘贴就能提交
2. 🔀 **PR 助手** — AI 分析 Bug 根因 → 定位代码 → 生成修复方案 → 通过 `run_command` 执行 git 命令完成 Fork/Branch/Commit/Push**用户只需点确认**
### 内置技能卡片
欢迎页提供一键触发的常用技能:
| 技能 | 功能 |
|------|------|
| 🔧 检查配置 | 读取并分析 openclaw.json |
| 🏥 诊断 Gateway | 检查进程、端口、日志 |
| 📂 浏览目录 | 查看 .openclaw 目录结构 |
| 💻 检查环境 | Node.js、npm 版本检测 |
| 📋 分析日志 | 搜索 ERROR/WARN 关键词 |
| 🔨 一键排障 | 自动检测并修复常见问题 |
| 🐛 提交 Bug | 整理 Issue 提交到 GitHub |
| 🔀 PR 助手 | 定位 Bug 并生成修复 PR |
## 技术架构
<p align="center">

BIN
docs/00.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

BIN
docs/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

BIN
docs/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

BIN
docs/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

BIN
docs/ai-assistant-demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 MiB

After

Width:  |  Height:  |  Size: 16 MiB

View File

@@ -4,25 +4,25 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ClawPanel - OpenClaw AI Agent 可视化管理面板 | 快速搭建、配置、监控你的 AI 智能体</title>
<meta name="description" content="ClawPanel 是 OpenClaw AI Agent 框架的可视化管理面板,基于 Tauri v2 构建的跨平台桌面应用。支持仪表盘监控、多模型配置OpenAI/DeepSeek/Kimi/Anthropic、实时 AI 聊天、记忆管理、Agent 管理、网关配置、服务管控、日志查看、内网穿透、系统诊断。一键安装 OpenClaw快速搭建你的私有 AI Agent开源免费支持 Windows/macOS/Linux。">
<meta name="description" content="ClawPanel 是 OpenClaw AI Agent 框架的可视化管理面板,基于 Tauri v2 构建的跨平台桌面应用。内置 AI 助手支持工具调用(终端执行、文件读写、目录浏览),让 AI 帮你诊断和修复 OpenClaw 配置。支持仪表盘监控、多模型配置OpenAI/DeepSeek/Kimi/Anthropic、实时 AI 聊天、记忆管理、Agent 管理、网关配置、服务管控、日志查看、内网穿透、系统诊断。一键安装 OpenClaw快速搭建你的私有 AI Agent开源免费支持 Windows/macOS/Linux。">
<meta name="keywords" content="ClawPanel, OpenClaw, AI Agent, AI 智能体, 管理面板, 可视化管理, 快速搭建, 一键安装, 桌面应用, 跨平台, Tauri, Tauri v2, Rust, 开源, 免费, LLM, 大语言模型, 多模型, 模型配置, OpenAI, DeepSeek, Kimi, Anthropic, Claude, 实时聊天, AI 对话, 流式响应, 记忆管理, Agent 管理, 多 Agent, 网关配置, Gateway, 服务管理, 日志查看, 内网穿透, Cloudflare Tunnel, 系统诊断, WebSocket, 仪表盘, 监控, 配置管理, 私有部署, 本地部署, 自托管, AI 工具, AI 平台, 智能体平台, 人工智能, 深度学习, 自然语言处理, NLP, 模型调度, 模型切换, 备选模型, 开箱即用, 零代码, 低代码, admin panel, dashboard, open source AI, self-hosted AI, local AI, AI management">
<meta name="author" content="晴辰云 QingchenCloud">
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large">
<link rel="canonical" href="https://claw.qt.cool/">
<meta property="og:title" content="ClawPanel - OpenClaw AI Agent 可视化管理面板 | 快速搭建你的 AI 智能体">
<meta property="og:description" content="基于 Tauri v2 的跨平台桌面应用,为 OpenClaw AI Agent 提供可视化管理。支持多模型配置、实时 AI 聊天、记忆管理、内网穿透等 10+ 功能模块。一键安装,开源免费。">
<meta property="og:description" content="基于 Tauri v2 的跨平台桌面应用,为 OpenClaw AI Agent 提供可视化管理。内置 AI 助手支持工具调用,让 AI 帮你诊断修复配置。支持多模型配置、实时聊天、记忆管理等 10+ 功能模块。开源免费。">
<meta property="og:type" content="website">
<meta property="og:url" content="https://claw.qt.cool/">
<meta property="og:site_name" content="ClawPanel">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://claw.qt.cool/01.png">
<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 仪表盘截图">
<meta property="og:image:alt" content="ClawPanel AI 助手截图">
<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 智能体。">
<meta name="twitter:image" content="https://claw.qt.cool/01.png">
<meta name="twitter:image" content="https://claw.qt.cool/00.png">
<link rel="icon" href="./logo.png" type="image/png">
<script type="application/ld+json">
{
@@ -46,7 +46,7 @@
"price": "0",
"priceCurrency": "CNY"
},
"screenshot": "https://claw.qt.cool/01.png",
"screenshot": "https://claw.qt.cool/00.png",
"keywords": "OpenClaw, AI Agent, 管理面板, Tauri, 跨平台, 开源, 免费, LLM, 多模型"
}
</script>
@@ -484,9 +484,9 @@
<div class="orb orb-1" id="orb1"></div>
<div class="orb orb-2" id="orb2"></div>
<div class="hero-inner">
<div class="reveal hero-badge"><span class="pulse"></span> v0.4.4 已发布 — Gateway 守护、配置同步、流式超时</div>
<h1 class="reveal hero-title">管理你的 <span class="gradient-text shimmer">AI Agent</span><br>从未如此直观</h1>
<p class="reveal hero-subtitle">ClawPanel 是 <strong>OpenClaw</strong> 的可视化管理面板,基于 Tauri v2 构建。<br>仪表盘、模型配置、实时聊天、记忆管理 — 一站式掌控</p>
<div class="reveal hero-badge"><span class="pulse"></span> 🤖 内置 AI 助手 — 一键安装、配置、诊断、修复 OpenClaw</div>
<h1 class="reveal hero-title"><span class="gradient-text shimmer">AI 助手</span>驱动的<br>OpenClaw 管理面板</h1>
<p class="reveal hero-subtitle">内置智能 AI 助手,帮你<strong>一键安装 OpenClaw</strong>、自动诊断配置、排查问题、修复错误。<br>8 大工具 + 4 种模式 + 交互式问答,从新手到老手都能轻松上手</p>
<div class="reveal hero-cta">
<a href="#download" class="btn btn-primary">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path d="M12 15V3m0 12l-4-4m4 4l4-4M2 17l.621 2.485A2 2 0 004.561 21h14.878a2 2 0 001.94-1.515L22 17"/></svg>
@@ -498,7 +498,7 @@
</a>
</div>
<div class="reveal hero-image-wrap" data-delay="300">
<img src="./01.png" alt="ClawPanel 仪表盘" onclick="openLightbox(this.src)" loading="eager">
<img src="./00.png" alt="ClawPanel AI 助手" onclick="openLightbox(this.src)" loading="eager">
<div class="hero-glow"></div>
</div>
</div>
@@ -536,7 +536,7 @@
<svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>
</div>
</div>
<video id="demoVideo" poster="./01.png" preload="metadata" playsinline controls onclick="toggleDemoVideo()">
<video id="demoVideo" poster="./video-cover.png" preload="metadata" playsinline controls onclick="toggleDemoVideo()">
<source src="./promo-web.mp4" type="video/mp4">
</video>
</div>
@@ -554,9 +554,73 @@
<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>
<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>
<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>
</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"><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>
<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>
</ul>
</div>
<div class="reveal screenshot-frame" onclick="openLightbox('./11.png')"><img src="./11.png" alt="AI 助手工具调用实战" loading="lazy"></div>
</div>
<!-- AI 助手设置 -->
<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>
<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>
<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>
</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 class="reveal screenshot-frame img-first" onclick="openLightbox('./02.png')"><img src="./02.png" alt="实时聊天" loading="lazy"></div>
<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>
@@ -568,6 +632,7 @@
<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>
<!-- 模型配置 -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

BIN
docs/video-cover-light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

BIN
docs/video-cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

View File

@@ -6,8 +6,68 @@
<title>ClawPanel</title>
<link rel="icon" href="/favicon.ico">
<!-- 样式由 main.js 通过 Vite 统一加载 -->
<style>
/* 启动加载屏(内联,零依赖,立即渲染) */
#splash {
position: fixed; inset: 0; z-index: 99999;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: #f8f9fb;
transition: opacity 0.4s ease, visibility 0.4s ease;
}
@media (prefers-color-scheme: dark) { #splash { background: #0f0f14; } }
#splash.hide { opacity: 0; visibility: hidden; pointer-events: none; }
#splash .sp-logo {
width: 56px; height: 56px; margin-bottom: 20px;
color: #6366f1; animation: sp-pulse 2s ease-in-out infinite;
}
#splash .sp-name {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 18px; font-weight: 600; letter-spacing: 0.5px;
color: #18181b; margin-bottom: 28px;
}
@media (prefers-color-scheme: dark) { #splash .sp-name { color: #e4e4e7; } }
#splash .sp-bar {
width: 120px; height: 3px; border-radius: 2px; overflow: hidden;
background: rgba(99, 102, 241, 0.15);
}
#splash .sp-bar-inner {
width: 40%; height: 100%; border-radius: 2px;
background: #6366f1;
animation: sp-slide 1.2s ease-in-out infinite;
}
#splash .sp-site {
margin-top: 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 12px; color: #a1a1aa; letter-spacing: 0.3px;
}
#splash .sp-site a {
color: #6366f1; text-decoration: none;
}
#splash .sp-site a:hover { text-decoration: underline; }
@keyframes sp-slide {
0% { transform: translateX(-100%); }
50% { transform: translateX(200%); }
100% { transform: translateX(-100%); }
}
@keyframes sp-pulse {
0%, 100% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
}
</style>
</head>
<body>
<!-- 启动加载屏 -->
<div id="splash">
<svg class="sp-logo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<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 class="sp-name">ClawPanel</div>
<div class="sp-bar"><div class="sp-bar-inner"></div></div>
<div class="sp-site"><a href="https://qt.cool" target="_blank">qt.cool</a></div>
</div>
<script>setTimeout(function(){var s=document.getElementById('splash');if(s){s.classList.add('hide');setTimeout(function(){s.remove()},500)}},6000)</script>
<div id="app">
<aside id="sidebar"></aside>
<div id="main-col">

View File

@@ -730,6 +730,50 @@ const handlers = {
},
}
},
// 数据目录 & 图片存储
assistant_ensure_data_dir() {
const dataDir = path.join(OPENCLAW_DIR, 'clawpanel')
for (const sub of ['images', 'sessions', 'cache']) {
const dir = path.join(dataDir, sub)
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
}
return dataDir
},
assistant_save_image({ id, data }) {
const dir = path.join(OPENCLAW_DIR, 'clawpanel', 'images')
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
const pureB64 = data.includes(',') ? data.split(',')[1] : data
const ext = data.startsWith('data:image/png') ? 'png'
: data.startsWith('data:image/gif') ? 'gif'
: data.startsWith('data:image/webp') ? 'webp' : 'jpg'
const filepath = path.join(dir, `${id}.${ext}`)
fs.writeFileSync(filepath, Buffer.from(pureB64, 'base64'))
return filepath
},
assistant_load_image({ id }) {
const dir = path.join(OPENCLAW_DIR, 'clawpanel', 'images')
for (const ext of ['jpg', 'png', 'gif', 'webp', 'jpeg']) {
const filepath = path.join(dir, `${id}.${ext}`)
if (fs.existsSync(filepath)) {
const bytes = fs.readFileSync(filepath)
const mime = ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif' : ext === 'webp' ? 'image/webp' : 'image/jpeg'
return `data:${mime};base64,${bytes.toString('base64')}`
}
}
throw new Error(`图片 ${id} 不存在`)
},
assistant_delete_image({ id }) {
const dir = path.join(OPENCLAW_DIR, 'clawpanel', 'images')
for (const ext of ['jpg', 'png', 'gif', 'webp', 'jpeg']) {
const filepath = path.join(dir, `${id}.${ext}`)
if (fs.existsSync(filepath)) fs.unlinkSync(filepath)
}
return null
},
check_panel_update() { return { latest: null, url: 'https://github.com/qingchencloud/clawpanel/releases' } },
write_env_file({ path: p, config }) {
const expanded = p.startsWith('~/') ? path.join(homedir(), p.slice(2)) : p

View File

@@ -0,0 +1,355 @@
/// AI 助手工具命令
/// 提供终端执行、文件读写、目录列表等能力
/// 仅在用户主动开启工具后由 AI 调用
use std::path::PathBuf;
use base64::{Engine as _, engine::general_purpose};
/// ClawPanel 数据目录(~/.openclaw/clawpanel/
fn data_dir() -> PathBuf {
super::openclaw_dir().join("clawpanel")
}
/// 确保数据目录及子目录存在,返回目录路径
#[tauri::command]
pub async fn assistant_ensure_data_dir() -> Result<String, String> {
let base = data_dir();
let subdirs = ["images", "sessions", "cache"];
for sub in &subdirs {
let dir = base.join(sub);
tokio::fs::create_dir_all(&dir)
.await
.map_err(|e| format!("创建目录 {} 失败: {e}", dir.display()))?;
}
Ok(base.to_string_lossy().to_string())
}
/// 保存图片base64 → 文件),返回文件路径
#[tauri::command]
pub async fn assistant_save_image(id: String, data: String) -> Result<String, String> {
let dir = data_dir().join("images");
tokio::fs::create_dir_all(&dir)
.await
.map_err(|e| format!("创建目录失败: {e}"))?;
// data 可能包含 data:image/xxx;base64, 前缀
let pure_b64 = if let Some(pos) = data.find(",") {
&data[pos + 1..]
} else {
&data
};
// 从 data URI 提取扩展名
let ext = if data.starts_with("data:image/png") {
"png"
} else if data.starts_with("data:image/gif") {
"gif"
} else if data.starts_with("data:image/webp") {
"webp"
} else {
"jpg"
};
let filename = format!("{}.{}", id, ext);
let filepath = dir.join(&filename);
let bytes = general_purpose::STANDARD
.decode(pure_b64)
.map_err(|e| format!("base64 解码失败: {e}"))?;
tokio::fs::write(&filepath, &bytes)
.await
.map_err(|e| format!("写入图片失败: {e}"))?;
Ok(filepath.to_string_lossy().to_string())
}
/// 加载图片(文件 → base64 data URI
#[tauri::command]
pub async fn assistant_load_image(id: String) -> Result<String, String> {
let dir = data_dir().join("images");
// 尝试各种扩展名
let mut found: Option<PathBuf> = None;
for ext in &["jpg", "png", "gif", "webp", "jpeg"] {
let path = dir.join(format!("{}.{}", id, ext));
if path.exists() {
found = Some(path);
break;
}
}
let filepath = found.ok_or_else(|| format!("图片 {} 不存在", id))?;
let bytes = tokio::fs::read(&filepath)
.await
.map_err(|e| format!("读取图片失败: {e}"))?;
let ext = filepath.extension().and_then(|e| e.to_str()).unwrap_or("jpg");
let mime = match ext {
"png" => "image/png",
"gif" => "image/gif",
"webp" => "image/webp",
_ => "image/jpeg",
};
let b64 = general_purpose::STANDARD.encode(&bytes);
Ok(format!("data:{};base64,{}", mime, b64))
}
/// 删除图片文件
#[tauri::command]
pub async fn assistant_delete_image(id: String) -> Result<(), String> {
let dir = data_dir().join("images");
for ext in &["jpg", "png", "gif", "webp", "jpeg"] {
let path = dir.join(format!("{}.{}", id, ext));
if path.exists() {
tokio::fs::remove_file(&path)
.await
.map_err(|e| format!("删除图片失败: {e}"))?;
}
}
Ok(())
}
// ── AI 助手工具 ──
/// 执行 shell 命令,返回 stdout + stderr
#[tauri::command]
pub async fn assistant_exec(command: String, cwd: Option<String>) -> Result<String, String> {
let work_dir = cwd.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_default()
.to_string_lossy()
.to_string()
});
let output;
#[cfg(target_os = "windows")]
{
const CREATE_NO_WINDOW: u32 = 0x08000000;
output = tokio::process::Command::new("cmd")
.args(["/c", &command])
.current_dir(&work_dir)
.env("PATH", super::enhanced_path())
.creation_flags(CREATE_NO_WINDOW)
.output()
.await
.map_err(|e| format!("执行失败: {e}"))?;
}
#[cfg(not(target_os = "windows"))]
{
output = tokio::process::Command::new("sh")
.args(["-c", &command])
.current_dir(&work_dir)
.env("PATH", super::enhanced_path())
.output()
.await
.map_err(|e| format!("执行失败: {e}"))?;
}
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let code = output.status.code().unwrap_or(-1);
let mut result = String::new();
if !stdout.is_empty() {
result.push_str(&stdout);
}
if !stderr.is_empty() {
if !result.is_empty() {
result.push('\n');
}
result.push_str("[stderr] ");
result.push_str(&stderr);
}
if result.is_empty() {
result = format!("(命令已执行,退出码: {code})");
} else if code != 0 {
result.push_str(&format!("\n(退出码: {code})"));
}
// 限制输出长度
if result.len() > 10000 {
result.truncate(10000);
result.push_str("\n...(输出已截断)");
}
Ok(result)
}
/// 读取文件内容
#[tauri::command]
pub async fn assistant_read_file(path: String) -> Result<String, String> {
let content = tokio::fs::read_to_string(&path)
.await
.map_err(|e| format!("读取文件失败 {path}: {e}"))?;
if content.len() > 50000 {
Ok(format!(
"{}...\n(文件内容已截断,共 {} 字节)",
&content[..50000],
content.len()
))
} else {
Ok(content)
}
}
/// 写入文件
#[tauri::command]
pub async fn assistant_write_file(path: String, content: String) -> Result<String, String> {
if let Some(parent) = PathBuf::from(&path).parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| format!("创建目录失败: {e}"))?;
}
tokio::fs::write(&path, &content)
.await
.map_err(|e| format!("写入文件失败 {path}: {e}"))?;
Ok(format!("已写入 {} ({} 字节)", path, content.len()))
}
/// 获取系统信息OS、架构、主目录、主机名
#[tauri::command]
pub async fn assistant_system_info() -> Result<String, String> {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let home = dirs::home_dir()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let hostname = std::env::var("COMPUTERNAME")
.or_else(|_| std::env::var("HOSTNAME"))
.unwrap_or_else(|_| "unknown".into());
let shell = if cfg!(target_os = "windows") {
"powershell / cmd"
} else if cfg!(target_os = "macos") {
"zsh (macOS default)"
} else {
"bash / sh"
};
Ok(format!(
"OS: {}\nArch: {}\nHome: {}\nHostname: {}\nShell: {}\nPath separator: {}",
os, arch, home, hostname, shell, std::path::MAIN_SEPARATOR
))
}
/// 列出运行中的进程(按名称过滤)
#[tauri::command]
pub async fn assistant_list_processes(filter: Option<String>) -> Result<String, String> {
let output = if cfg!(target_os = "windows") {
tokio::process::Command::new("powershell")
.args(["-NoProfile", "-Command",
"Get-Process | Select-Object Id, ProcessName, CPU, WorkingSet64 | Sort-Object ProcessName | Format-Table -AutoSize | Out-String -Width 200"])
.output()
.await
} else {
tokio::process::Command::new("ps")
.args(["aux", "--sort=-%mem"])
.output()
.await
};
let output = output.map_err(|e| format!("获取进程列表失败: {e}"))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if let Some(f) = filter {
let f_lower = f.to_lowercase();
let lines: Vec<&str> = stdout.lines()
.filter(|line| {
let lower = line.to_lowercase();
lower.contains(&f_lower) || lower.starts_with("id") || lower.starts_with("user") || lower.contains("---")
})
.collect();
if lines.len() <= 2 {
return Ok(format!("未找到匹配 '{}' 的进程", f));
}
Ok(lines.join("\n"))
} else {
// 无过滤时限制输出行数
let lines: Vec<&str> = stdout.lines().take(80).collect();
Ok(lines.join("\n"))
}
}
/// 检测端口是否在监听
#[tauri::command]
pub async fn assistant_check_port(port: u16) -> Result<String, String> {
use std::time::Duration;
let addr = format!("127.0.0.1:{}", port);
let result = std::net::TcpStream::connect_timeout(
&addr.parse().map_err(|e| format!("地址解析失败: {e}"))?,
Duration::from_secs(2),
);
match result {
Ok(_stream) => {
// 尝试获取占用进程信息
let process_info = get_port_process(port).await;
Ok(format!("端口 {} 已被占用(正在监听){}", port, process_info))
}
Err(_) => Ok(format!("端口 {} 未被占用(空闲)", port)),
}
}
async fn get_port_process(port: u16) -> String {
let output = if cfg!(target_os = "windows") {
tokio::process::Command::new("powershell")
.args(["-NoProfile", "-Command",
&format!("Get-NetTCPConnection -LocalPort {} -ErrorAction SilentlyContinue | Select-Object OwningProcess | ForEach-Object {{ (Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName }}", port)])
.output()
.await
} else {
tokio::process::Command::new("lsof")
.args(["-i", &format!(":{}", port), "-t"])
.output()
.await
};
match output {
Ok(o) => {
let s = String::from_utf8_lossy(&o.stdout).trim().to_string();
if s.is_empty() { String::new() } else { format!("\n占用进程: {}", s) }
}
Err(_) => String::new(),
}
}
/// 列出目录内容
#[tauri::command]
pub async fn assistant_list_dir(path: String) -> Result<String, String> {
let mut entries = tokio::fs::read_dir(&path)
.await
.map_err(|e| format!("读取目录失败 {path}: {e}"))?;
let mut items = Vec::new();
while let Some(entry) = entries
.next_entry()
.await
.map_err(|e| format!("{e}"))?
{
let meta = entry.metadata().await.ok();
let name = entry.file_name().to_string_lossy().to_string();
let is_dir = meta.as_ref().map(|m| m.is_dir()).unwrap_or(false);
let size = meta.as_ref().map(|m| m.len()).unwrap_or(0);
if is_dir {
items.push(format!("[DIR] {}/", name));
} else {
items.push(format!("[FILE] {} ({} bytes)", name, size));
}
if items.len() >= 200 {
items.push("...(已截断)".into());
break;
}
}
items.sort();
Ok(items.join("\n"))
}

View File

@@ -1,6 +1,7 @@
use std::path::PathBuf;
pub mod agent;
pub mod assistant;
pub mod config;
pub mod device;
pub mod extensions;

View File

@@ -3,7 +3,7 @@ mod models;
mod tray;
mod utils;
use commands::{agent, config, device, extensions, logs, memory, pairing, service};
use commands::{agent, assistant, config, device, extensions, logs, memory, pairing, service};
pub fn run() {
tauri::Builder::default()
@@ -74,6 +74,19 @@ pub fn run() {
agent::update_agent_identity,
agent::update_agent_model,
agent::backup_agent,
// AI 助手工具
assistant::assistant_exec,
assistant::assistant_read_file,
assistant::assistant_write_file,
assistant::assistant_list_dir,
assistant::assistant_system_info,
assistant::assistant_list_processes,
assistant::assistant_check_port,
// 数据目录 & 图片存储
assistant::assistant_ensure_data_dir,
assistant::assistant_save_image,
assistant::assistant_load_image,
assistant::assistant_delete_image,
])
.run(tauri::generate_context!())
.expect("启动 ClawPanel 失败");

View File

@@ -10,6 +10,7 @@ const NAV_ITEMS_FULL = [
section: '概览',
items: [
{ route: '/dashboard', label: '仪表盘', icon: 'dashboard' },
{ route: '/assistant', label: 'AI 助手', icon: 'assistant' },
{ route: '/chat', label: '实时聊天', icon: 'chat' },
{ route: '/services', label: '服务管理', icon: 'services' },
{ route: '/logs', label: '日志查看', icon: 'logs' },
@@ -38,8 +39,8 @@ const NAV_ITEMS_FULL = [
{
section: '',
items: [
{ route: '/about', label: '关于', icon: 'about' },
{ route: '/chat-debug', label: '系统诊断', icon: 'debug' },
{ route: '/about', label: '关于', icon: 'about' },
]
}
]
@@ -49,6 +50,7 @@ const NAV_ITEMS_SETUP = [
section: '',
items: [
{ route: '/setup', label: '初始设置', icon: 'setup' },
{ route: '/assistant', label: 'AI 助手', icon: 'assistant' },
]
},
{
@@ -60,8 +62,8 @@ const NAV_ITEMS_SETUP = [
{
section: '',
items: [
{ route: '/about', label: '关于', icon: 'about' },
{ route: '/chat-debug', label: '系统诊断', icon: 'debug' },
{ route: '/about', label: '关于', icon: 'about' },
]
}
]
@@ -78,6 +80,7 @@ const ICONS = {
memory: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
extensions: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>',
about: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"/></svg>',
assistant: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>',
debug: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/><circle cx="12" cy="12" r="3"/></svg>',
}

View File

@@ -17,18 +17,24 @@ const KEYWORDS = new Set([
function highlightCode(code, lang) {
const escaped = escapeHtml(code)
// Two-phase: mark with control chars first, convert to HTML last
// Prevents keyword regex from matching "class" inside <span class="..."> attributes
const S = '\x02', E = '\x03'
const CLS = ['hl-number','hl-comment','hl-string','hl-type','hl-func','hl-keyword']
return escaped
.replace(/\b(\d+\.?\d*)\b/g, '<span class="hl-number">$1</span>')
.replace(/(\/\/.*$|#.*$)/gm, '<span class="hl-comment">$1</span>')
.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="hl-comment">$1</span>')
.replace(/\b(\d+\.?\d*)\b/g, `${S}0${E}$1${S}c${E}`)
.replace(/(\/\/.*$|#.*$)/gm, `${S}1${E}$1${S}c${E}`)
.replace(/(\/\*[\s\S]*?\*\/)/g, `${S}1${E}$1${S}c${E}`)
.replace(/(&quot;(?:[^&]|&(?!quot;))*?&quot;|&#x27;(?:[^&]|&(?!#x27;))*?&#x27;|`[^`]*`)/g,
'<span class="hl-string">$1</span>')
`${S}2${E}$1${S}c${E}`)
.replace(/\b([A-Z][a-zA-Z0-9_]*)\b/g, (m, w) =>
KEYWORDS.has(w) ? m : `<span class="hl-type">${w}</span>`)
KEYWORDS.has(w) ? m : `${S}3${E}${w}${S}c${E}`)
.replace(/\b(\w+)(?=\s*\()/g, (m, w) =>
KEYWORDS.has(w) ? m : `<span class="hl-func">${w}</span>`)
KEYWORDS.has(w) ? m : `${S}4${E}${w}${S}c${E}`)
.replace(/\b(\w+)\b/g, (m, w) =>
KEYWORDS.has(w) ? `<span class="hl-keyword">${w}</span>` : m)
KEYWORDS.has(w) ? `${S}5${E}${w}${S}c${E}` : m)
.replace(/\x02([0-5])\x03/g, (_, i) => `<span class="${CLS[+i]}">`)
.replace(/\x02c\x03/g, '</span>')
}
function escapeHtml(str) {

View File

@@ -14,6 +14,7 @@ const NO_MOCK_CMDS = new Set([
'write_memory_file', 'delete_memory_file',
'set_npm_registry', 'reload_gateway', 'restart_gateway',
'auto_pair_device',
'assistant_exec', 'assistant_write_file',
])
// 预加载 Tauri invoke避免每次 API 调用都做动态 import
@@ -236,6 +237,19 @@ function mockInvoke(cmd, args) {
cftunnel_action: () => true,
get_cftunnel_logs: () => '2026-02-26 13:29:01 [INFO] Tunnel started\n2026-02-26 13:30:00 [INFO] Connection healthy',
get_clawapp_status: () => ({ running: true, pid: 7752, port: 3210, url: 'http://localhost:3210' }),
// AI 助手工具
assistant_exec: ({ command }) => `[mock] 执行: ${command}\n这是模拟输出`,
assistant_read_file: ({ path }) => `[mock] 文件内容: ${path}\n# 示例文件\n这是模拟文件内容`,
assistant_write_file: ({ path, content }) => `已写入 ${path} (${content.length} 字节)`,
assistant_list_dir: ({ path }) => '[DIR] src/\n[DIR] docs/\n[FILE] README.md (1024 bytes)\n[FILE] package.json (512 bytes)',
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_ensure_data_dir: () => (navigator.platform.includes('Win') ? 'C:\\Users\\user\\.openclaw\\clawpanel' : '/Users/user/.openclaw/clawpanel'),
assistant_save_image: ({ id }) => `/mock/images/${id}.jpg`,
assistant_load_image: () => 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQABNjN9GQAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAAA0lEQVQI12P4z8BQDwAEgAF/QualzQAAAABJRU5ErkJggg==',
assistant_delete_image: () => null,
}
const fn = mocks[cmd]
return fn ? Promise.resolve(fn(args)) : Promise.reject(`未知命令: ${cmd}`)
@@ -316,4 +330,19 @@ export const api = {
// 设备配对
autoPairDevice: () => invoke('auto_pair_device'),
checkPairingStatus: () => invoke('check_pairing_status'),
// AI 助手工具
assistantExec: (command, cwd) => invoke('assistant_exec', { command, cwd: cwd || null }),
assistantReadFile: (path) => invoke('assistant_read_file', { path }),
assistantWriteFile: (path, content) => invoke('assistant_write_file', { path, content }),
assistantListDir: (path) => invoke('assistant_list_dir', { path }),
assistantSystemInfo: () => invoke('assistant_system_info'),
assistantListProcesses: (filter) => invoke('assistant_list_processes', { filter: filter || null }),
assistantCheckPort: (port) => invoke('assistant_check_port', { port }),
// 数据目录 & 图片存储
ensureDataDir: () => invoke('assistant_ensure_data_dir'),
saveImage: (id, data) => invoke('assistant_save_image', { id, data }),
loadImage: (id) => invoke('assistant_load_image', { id }),
deleteImage: (id) => invoke('assistant_delete_image', { id }),
}

View File

@@ -17,6 +17,7 @@ import './style/pages.css'
import './style/chat.css'
import './style/agents.css'
import './style/debug.css'
import './style/assistant.css'
// 初始化主题
initTheme()
@@ -37,11 +38,19 @@ async function boot() {
registerRoute('/memory', () => import('./pages/memory.js'))
registerRoute('/extensions', () => import('./pages/extensions.js'))
registerRoute('/about', () => import('./pages/about.js'))
registerRoute('/assistant', () => import('./pages/assistant.js'))
registerRoute('/setup', () => import('./pages/setup.js'))
renderSidebar(sidebar)
initRouter(content)
// 隐藏启动加载屏
const splash = document.getElementById('splash')
if (splash) {
splash.classList.add('hide')
setTimeout(() => splash.remove(), 500)
}
// 后台检测状态,检测完再决定是否跳转 setup
detectOpenclawStatus().then(() => {
// 重新渲染侧边栏(检测完成后 isOpenclawReady 状态已更新)

2715
src/pages/assistant.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -703,21 +703,34 @@ function handleChatEvent(payload) {
appendAudiosToEl(_currentAiBubble, _currentAiAudios)
appendFilesToEl(_currentAiBubble, _currentAiFiles)
}
// 添加时间戳 + 耗时
// 添加时间戳 + 耗时 + token 消耗
const wrapper = _currentAiBubble?.parentElement
if (wrapper) {
const time = document.createElement('div')
time.className = 'msg-time'
let timeStr = formatTime(new Date())
const meta = document.createElement('div')
meta.className = 'msg-meta'
let parts = [`<span class="msg-time">${formatTime(new Date())}</span>`]
// 计算响应耗时
let durStr = ''
if (payload.durationMs) {
timeStr += ` · ${(payload.durationMs / 1000).toFixed(1)}s`
durStr = (payload.durationMs / 1000).toFixed(1) + 's'
} else if (_streamStartTime) {
const dur = ((Date.now() - _streamStartTime) / 1000).toFixed(1)
timeStr += ` · ${dur}s`
durStr = ((Date.now() - _streamStartTime) / 1000).toFixed(1) + 's'
}
time.textContent = timeStr
wrapper.appendChild(time)
if (durStr) parts.push(`<span class="meta-sep">·</span><span class="msg-duration">⏱ ${durStr}</span>`)
// token 消耗(从 payload.usage 或 payload.message.usage 提取)
const usage = payload.usage || payload.message?.usage || null
if (usage) {
const inp = usage.input_tokens || usage.prompt_tokens || 0
const out = usage.output_tokens || usage.completion_tokens || 0
const total = usage.total_tokens || (inp + out)
if (total > 0) {
let tokenStr = `${total} tokens`
if (inp && out) tokenStr = `${inp}${out}`
parts.push(`<span class="meta-sep">·</span><span class="msg-tokens">${tokenStr}</span>`)
}
}
meta.innerHTML = parts.join('')
wrapper.appendChild(meta)
}
if (_currentAiText || _currentAiImages.length) {
saveMessage({
@@ -1101,12 +1114,12 @@ function appendUserMessage(text, attachments = [], msgTime) {
bubble.appendChild(textNode)
}
const time = document.createElement('div')
time.className = 'msg-time'
time.textContent = formatTime(msgTime || new Date())
const meta = document.createElement('div')
meta.className = 'msg-meta'
meta.innerHTML = `<span class="msg-time">${formatTime(msgTime || new Date())}</span>`
wrap.appendChild(bubble)
wrap.appendChild(time)
wrap.appendChild(meta)
_messagesEl.insertBefore(wrap, _typingEl)
scrollToBottom()
}
@@ -1124,12 +1137,12 @@ function appendAiMessage(text, msgTime, images, videos, audios, files) {
// 图片点击灯箱
bubble.querySelectorAll('img').forEach(img => { if (!img.onclick) img.onclick = () => showLightbox(img.src) })
const time = document.createElement('div')
time.className = 'msg-time'
time.textContent = formatTime(msgTime || new Date())
const meta = document.createElement('div')
meta.className = 'msg-meta'
meta.innerHTML = `<span class="msg-time">${formatTime(msgTime || new Date())}</span>`
wrap.appendChild(bubble)
wrap.appendChild(time)
wrap.appendChild(meta)
_messagesEl.insertBefore(wrap, _typingEl)
scrollToBottom()
}

View File

@@ -149,6 +149,29 @@ function renderSteps(page, { node, cliOk, config }) {
</div>
`
// AI 助手入口
html += `
<div class="config-section" style="text-align:left;margin-top:var(--space-md)">
<div class="config-section-title" style="display:flex;align-items:center;gap:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><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"/></svg>
晴辰助手
</div>
<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm);line-height:1.5">
遇到安装问题AI 助手可以帮你诊断和解决。配置好模型后,点击下方按钮${!allOk ? ',当前问题会自动发送给 AI 分析' : ''}
</p>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button class="btn btn-secondary btn-sm" id="btn-goto-assistant">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px"><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"/></svg>
打开 AI 助手
</button>
${!allOk ? `<button class="btn btn-primary btn-sm" id="btn-ask-ai-help">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="margin-right:4px"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
让 AI 帮我解决
</button>` : ''}
</div>
</div>
`
// 全部就绪 → 进入面板
if (allOk) {
html += `
@@ -159,7 +182,7 @@ function renderSteps(page, { node, cliOk, config }) {
}
stepsEl.innerHTML = html
bindEvents(page, nodeOk)
bindEvents(page, nodeOk, { node, cliOk, config })
}
function renderInstallSection() {
@@ -195,7 +218,37 @@ function renderInstallSection() {
`
}
function bindEvents(page, nodeOk) {
function buildSetupProblemPrompt({ node, cliOk, config }) {
const problems = []
if (!node.installed) problems.push('- Node.js 未安装或未检测到')
else problems.push(`- Node.js 已安装: ${node.version || '版本未知'}`)
if (!cliOk) problems.push('- OpenClaw CLI 未安装')
else problems.push('- OpenClaw CLI 已安装')
if (!config.installed) problems.push('- 配置文件不存在')
else problems.push(`- 配置文件正常: ${config.path || ''}`)
return `我在安装 OpenClaw 时遇到问题,以下是当前检测状态:
${problems.join('\n')}
请帮我分析问题并给出解决步骤。如果需要,请使用工具帮我检查系统环境。`
}
function bindEvents(page, nodeOk, detectState) {
// 打开 AI 助手
page.querySelector('#btn-goto-assistant')?.addEventListener('click', () => {
window.location.hash = '/assistant'
})
// 让 AI 帮我解决(带问题上下文)
page.querySelector('#btn-ask-ai-help')?.addEventListener('click', () => {
if (detectState) {
const prompt = buildSetupProblemPrompt(detectState)
sessionStorage.setItem('assistant-auto-prompt', prompt)
}
window.location.hash = '/assistant'
})
// 进入面板
page.querySelector('#btn-enter')?.addEventListener('click', () => {
window.location.hash = '/dashboard'

1522
src/style/assistant.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -128,6 +128,7 @@
/* 消息通用 */
.msg {
display: flex;
flex-direction: column;
max-width: 85%;
animation: msg-in 0.2s ease-out;
}
@@ -687,15 +688,38 @@
backdrop-filter: blur(4px);
}
/* 消息时间戳 */
.msg-time {
/* 消息时间戳 + 元信息 */
.msg-meta {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-tertiary);
margin-top: 4px;
padding: 0 4px;
flex-wrap: wrap;
}
.msg-user .msg-meta { justify-content: flex-end; }
.msg-ai .msg-meta { justify-content: flex-start; }
.msg-meta .msg-time {
font-size: 11px;
}
.msg-meta .msg-tokens {
font-size: 10px;
opacity: 0.8;
}
.msg-meta .msg-duration {
font-size: 10px;
opacity: 0.8;
}
.msg-meta .meta-sep {
color: var(--text-tertiary);
opacity: 0.4;
}
.msg-user .msg-time { text-align: right; }
.msg-ai .msg-time { text-align: left; }
/* 消息内图片 */
.msg-img {

View File

@@ -160,7 +160,7 @@
/* Toast 通知 */
.toast-container {
position: fixed;
top: var(--space-lg);
top: 56px;
right: var(--space-lg);
z-index: 9999;
display: flex;