diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3e8bd..d7591e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.cool),GPT-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) diff --git a/README.md b/README.md index 88c2672..642d97a 100644 --- a/README.md +++ b/README.md @@ -127,77 +127,77 @@ docker run -d --name clawpanel --restart unless-stopped \

- AI 助手 + AI 助手

🤖 AI 助手 — 8 大技能卡片,一键触发配置检查、Gateway 诊断、环境检测、一键排障等常用操作

- AI 助手工具调用实战 + 仪表盘

-

🔧 AI 实战 — 自动调用工具:获取系统信息 → 列出目录 → 读取配置 → 生成健康检查报告,全程可视化

+

仪表盘 — Gateway / 隧道 / 服务实时状态,版本信息、Agent 数量、模型池一屏掌握

- AI 助手设置 + AI 助手设置 — 公益 AI 接口

-

⚙️ AI 设置 — 独立模型配置,支持任意 OpenAI 兼容 API,无需安装 OpenClaw 也能使用 AI 助手

+

⚙️ AI 设置 — 独立模型配置 + 公益 AI 接口一键接入,GPT-5 全系列免费可用

- AI 图片识别 + AI 助手人设 — Agent 灵魂

-

🖼️ 图片识别 — 粘贴截图或拖拽图片,AI 自动识别分析内容,多模态图文混排对话

+

� 借尸还魂 — 从 OpenClaw Agent 加载灵魂(SOUL / IDENTITY / USER / AGENTS / TOOLS),继承人格与记忆

- 仪表盘 + 实时聊天

-

仪表盘 — 系统运行概览,服务状态一目了然

+

实时聊天 — WebSocket 流式对话,多 Provider 模型自动聚合,支持多模态

- 实时聊天 + 模型配置

-

实时聊天 — WebSocket 流式对话,支持 Markdown 渲染与多会话管理

+

模型配置 — 多服务商统一管理,公益接口一键添加全部模型,主模型+备选自动切换

- 模型配置 + 记忆文件

-

模型配置 — 多服务商管理,主模型+备选自动切换

- -

- 记忆文件 -

-

记忆文件 — 在线编辑 Agent 核心配置与工作记忆

+

记忆文件 — 工作记忆、记忆归档、核心文件在线编辑,多 Agent 记忆隔离

查看更多截图

- Agent 管理 + Agent 管理

-

Agent 管理 — 多 Agent 创建、身份配置与工作区管理

+

Agent 管理 — 多 Agent 创建、身份配置与独立工作区管理

- Gateway 配置 + Gateway 安全认证

-

Gateway 配置 — 端口、访问权限、认证方式可视化配置

+

Gateway — Token / 密码双认证,Agent 工具权限三档管控,会话可见性控制

服务管理

-

服务管理 — 启停控制、版本检测、一键升级、配置备份

+

服务管理 — 启停控制、版本检测、一键升级、npm 源切换、配置备份

- 日志查看 + 安全设置

-

日志查看 — 多日志源实时查看与关键词搜索

+

安全设置 — 访问密码保护与无视风险模式

- 扩展工具 + 扩展工具

-

扩展工具 — cftunnel 内网穿透、ClawApp 移动客户端管理

+

扩展工具 — cftunnel 内网穿透、ClawApp 移动客户端一键安装

- 系统诊断 + 系统诊断

-

系统诊断 — 全面健康检测与一键修复

+

系统诊断 — 全面健康检测、WebSocket 测试、一键修复配对

+ +

+ 关于 +

+

关于 — 版本信息、社群入口(QQ / 微信 / 抖音)、相关项目链接

diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..76a7f84 --- /dev/null +++ b/deploy.sh @@ -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 "" diff --git a/docs/00.png b/docs/00.png index ab9ec6c..e30e994 100644 Binary files a/docs/00.png and b/docs/00.png differ diff --git a/docs/01.png b/docs/01.png index bc047be..0ccefa1 100644 Binary files a/docs/01.png and b/docs/01.png differ diff --git a/docs/02.png b/docs/02.png index 78b6508..d68601d 100644 Binary files a/docs/02.png and b/docs/02.png differ diff --git a/docs/04.png b/docs/04.png index d2e3de2..5740273 100644 Binary files a/docs/04.png and b/docs/04.png differ diff --git a/docs/05.png b/docs/05.png index 4bd9a98..462ee6f 100644 Binary files a/docs/05.png and b/docs/05.png differ diff --git a/docs/06.png b/docs/06.png index c2cbeb4..472c747 100644 Binary files a/docs/06.png and b/docs/06.png differ diff --git a/docs/07.png b/docs/07.png index 5387235..88fe722 100644 Binary files a/docs/07.png and b/docs/07.png differ diff --git a/docs/08.png b/docs/08.png index cac22c4..d21ebab 100644 Binary files a/docs/08.png and b/docs/08.png differ diff --git a/docs/09.png b/docs/09.png index 9996e3a..319af7c 100644 Binary files a/docs/09.png and b/docs/09.png differ diff --git a/docs/10.png b/docs/10.png index be153e0..88fb779 100644 Binary files a/docs/10.png and b/docs/10.png differ diff --git a/docs/11.png b/docs/11.png index 7734664..544688f 100644 Binary files a/docs/11.png and b/docs/11.png differ diff --git a/docs/12.png b/docs/12.png index f31477f..c99023c 100644 Binary files a/docs/12.png and b/docs/12.png differ diff --git a/docs/13.png b/docs/13.png index 401bf08..fce33bf 100644 Binary files a/docs/13.png and b/docs/13.png differ diff --git a/docs/14.png b/docs/14.png new file mode 100644 index 0000000..177d51a Binary files /dev/null and b/docs/14.png differ diff --git a/docs/15.png b/docs/15.png new file mode 100644 index 0000000..3002698 Binary files /dev/null and b/docs/15.png differ diff --git a/docs/16.png b/docs/16.png new file mode 100644 index 0000000..c080717 Binary files /dev/null and b/docs/16.png differ diff --git a/docs/assistant-features-plan.md b/docs/assistant-features-plan.md new file mode 100644 index 0000000..1231a26 --- /dev/null +++ b/docs/assistant-features-plan.md @@ -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) + +#### 方案 A:DuckDuckGo Instant Answer API(推荐,免费无 Key) +```js +// 搜索 +const resp = await fetch(`https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json`) +// 但 Instant Answer 只返回摘要,不返回搜索结果列表 + +// 实际搜索需要用 DuckDuckGo HTML 页面解析或第三方库 +``` + +#### 方案 B:SearXNG 代理(自托管,最灵活) +```js +// 部署一个 SearXNG 实例,或者用公共实例 +const resp = await fetch(`https://searx.example.com/search?q=${query}&format=json`) +``` + +#### 方案 C:Jina 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//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 按需检索 + +### 优先级:� 高(灵魂移植是杀手级差异化功能) +### 工时估算:灵魂移植 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" + } + } + } +} +``` + +**层级 2:Agent 模型注册表** `~/.openclaw/agents//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) | 系统提示词新增各工具使用指南 | diff --git a/docs/index.html b/docs/index.html index 900aa62..f16293b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -18,7 +18,7 @@ - + @@ -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 @@ 部署 文档 社区 + 活动 下载 GitHub ` @@ -286,7 +288,7 @@ function setupGatewayBanner() { banner.classList.remove('gw-banner-hidden') banner.innerHTML = `
- + ${statusIcon('warn', 16)} Gateway 未启动,部分功能不可用
@@ -302,7 +304,7 @@ function setupGatewayBanner() { const errMsg = err.message || String(err) banner.innerHTML = `
- + ${statusIcon('warn', 16)} 启动失败: ${errMsg} 查看日志 @@ -331,7 +333,7 @@ function setupGatewayBanner() { } catch {} banner.innerHTML = `
- + ${statusIcon('warn', 16)} 启动超时,Gateway 可能仍在启动中 查看日志 @@ -353,7 +355,7 @@ function showGuardianRecovery() { banner.classList.remove('gw-banner-hidden') banner.innerHTML = `
- 🛠 + ${statusIcon('warn', 16)} Gateway 反复启动失败,可能配置有误 @@ -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) })() diff --git a/src/pages/about.js b/src/pages/about.js index 7529f64..e144893 100644 --- a/src/pages/about.js +++ b/src/pages/about.js @@ -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) { 微信交流群
微信交流群
+
+ 抖音交流群 +
抖音交流群
+
扫码或点击链接加入交流群,反馈问题、获取帮助
@@ -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 + 代理服务器架构,支持离线和流式传输', diff --git a/src/pages/assistant.js b/src/pages/assistant.js index 95aea52..b53af3e 100644 --- a/src/pages/assistant.js +++ b/src/pages/assistant.js @@ -7,6 +7,8 @@ import { renderMarkdown } from '../lib/markdown.js' import { toast } from '../components/toast.js' import { showConfirm } from '../components/modal.js' import { api } from '../lib/tauri-api.js' +import { OPENCLAW_KB } from '../lib/openclaw-kb.js' +import { icon, statusIcon } from '../lib/icons.js' // ── 常量 ── const STORAGE_KEY = 'clawpanel-assistant' @@ -14,6 +16,26 @@ const SESSIONS_KEY = 'clawpanel-assistant-sessions' const MAX_SESSIONS = 50 const MAX_CONTEXT_TOKENS = 30 // 最近 N 条消息作为上下文 +// ── 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=', + models: [ + { id: 'gpt-5.4', name: 'GPT-5.4', hot: true }, + { id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex' }, + { id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex' }, + { id: 'gpt-5.2', name: 'GPT-5.2' }, + { id: 'gpt-5.1-codex-max', name: 'GPT-5.1 Codex Max' }, + { id: 'gpt-5.1-codex-mini', name: 'GPT-5.1 Codex Mini' }, + { id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex' }, + { id: 'gpt-5.1', name: 'GPT-5.1' }, + { id: 'gpt-5-codex', name: 'GPT-5 Codex' }, + { id: 'gpt-5', name: 'GPT-5' }, + ] +} + // ── 图片文件存储(通过 Tauri 后端持久化到 ~/.openclaw/clawpanel/images/)── async function saveImageToFile(id, dataUrl) { try { await api.saveImage(id, dataUrl) } catch (e) { console.warn('图片保存失败:', e) } @@ -74,11 +96,10 @@ ${personality} - **开源项目**: - **ClawPanel** — OpenClaw 可视化管理面板(Tauri v2),官网 https://claw.qt.cool - **OpenClaw 汉化版** — AI Agent 平台中文版,npm install -g @qingchencloud/openclaw-zh - - **ClawApp** — OpenClaw 移动客户端(iOS/Android),npm install -g @anthropic-ai/claw-app - - **cftunnel** — Cloudflare Tunnel CLI 封装工具,一键内网穿透 + - **ClawApp** — OpenClaw 手机聊天客户端(H5/PWA),通过一键脚本部署,GitHub: https://github.com/qingchencloud/clawapp + - **cftunnel** — 全协议内网穿透 CLI(Cloudflare Tunnel + frp 双引擎,Go 编写),GitHub: https://github.com/qingchencloud/cftunnel - **cfsite** — Cloudflare Pages 部署 CLI - **WebToEXE** — 网站打包成桌面应用 - - **AppForge** — 网站打包 SaaS 平台,https://app.qrj.ai ## ClawPanel 是什么 - OpenClaw 的可视化管理面板,基于 Tauri v2 的跨平台桌面应用(Windows/macOS/Linux) @@ -89,7 +110,7 @@ ${personality} - 开源的 AI Agent 平台,支持多模型、多 Agent、MCP 工具调用 - 核心组件: Gateway(API 网关)、Agent(AI 代理)、Tools(工具系统) - 配置文件: ~/.openclaw/openclaw.json(全局配置) -- 安装方式: npm install -g @qingchencloud/openclaw-zh(汉化版)或 npm install -g @anthropic-ai/openclaw(官方版) +- 安装方式: npm install -g @qingchencloud/openclaw-zh(汉化版,推荐)或 npm install -g openclaw(官方英文版) ## OpenClaw CLI 命令速查 ### 基础命令 @@ -138,11 +159,16 @@ ${personality} ## 生态项目安装指引 当用户问到如何安装其他产品时,推荐以下安装方式: -- **OpenClaw 汉化版**: npm install -g @qingchencloud/openclaw-zh -- **OpenClaw 官方版**: npm install -g @anthropic-ai/openclaw -- **ClawApp 移动客户端**: npm install -g @anthropic-ai/claw-app +- **OpenClaw 汉化版**: npm install -g @qingchencloud/openclaw-zh(推荐国内用户) +- **OpenClaw 官方版**: npm install -g openclaw +- **ClawApp 手机客户端**: + - Mac/Linux: curl -fsSL https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.sh | bash + - Windows: irm https://raw.githubusercontent.com/qingchencloud/clawapp/main/install.ps1 | iex + - 或手动: git clone https://github.com/qingchencloud/clawapp.git && cd clawapp && npm run install:all && npm run build:h5 - **ClawPanel**: 从 https://github.com/qingchencloud/clawpanel/releases 下载 -- **cftunnel**: 从 https://github.com/qingchencloud/cftunnel/releases 下载 +- **cftunnel 内网穿透**: + - Mac/Linux: curl -fsSL https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.sh | bash + - Windows: irm https://raw.githubusercontent.com/qingchencloud/cftunnel/main/install.ps1 | iex - **更多项目**: 访问 https://github.com/qingchencloud ## 社区贡献指引 @@ -205,6 +231,14 @@ Issue 模板(帮用户填好): 注意:每个选项应该简短明了,不要超过 4 个选项(用户可以输入自定义内容)。 +## web_search / fetch_url 使用指南 +当你无法确定答案或需要最新信息时,可以使用 web_search 搜索互联网: +- 搜索错误信息时,用引号包裹关键错误文本 +- 加 site:github.com 搜索 GitHub Issues +- 加 site:stackoverflow.com 搜索 StackOverflow +- 搜索后如需更多细节,用 fetch_url 抓取具体页面内容 +- fetch_url 返回纯文本格式,大页面会截断到 100KB + ## 你的工作方式 - 用中文回复 - 如果用户粘贴了日志,仔细分析每一行,找出关键错误 @@ -297,6 +331,37 @@ const TOOL_DEFS = { }, }, ], + webSearch: [ + { + type: 'function', + function: { + name: 'web_search', + description: '联网搜索关键词,返回搜索结果列表(标题、链接、摘要)。用于查找错误解决方案、最新文档、GitHub Issues 等。', + parameters: { + type: 'object', + properties: { + query: { type: 'string', description: '搜索关键词' }, + max_results: { type: 'integer', description: '最大结果数(默认 5)' }, + }, + required: ['query'], + }, + }, + }, + { + type: 'function', + function: { + name: 'fetch_url', + description: '抓取指定 URL 的网页内容,返回纯文本/Markdown 格式。用于获取搜索结果中某个页面的详细内容。', + parameters: { + type: 'object', + properties: { + url: { type: 'string', description: '要抓取的网页 URL' }, + }, + required: ['url'], + }, + }, + }, + ], fileOps: [ { type: 'function', @@ -378,129 +443,137 @@ function isCriticalCommand(command) { const BUILTIN_SKILLS = [ { id: 'check-config', - icon: '🔧', + icon: icon('wrench', 16), name: '检查 OpenClaw 配置', desc: '读取并分析 openclaw.json,检查配置是否正确', tools: ['fileOps'], prompt: `请帮我检查 OpenClaw 的配置文件。 具体操作: -1. 先调用 get_system_info 获取系统信息,确定配置目录路径 -2. 用 list_directory 查看 OpenClaw 配置目录(.openclaw/)结构 -3. 用 read_file 读取 openclaw.json +1. 调用 get_system_info 获取系统信息,确定主目录和 OS 类型 +2. 用 list_directory 查看 ~/.openclaw/ 目录结构 +3. 用 read_file 读取 ~/.openclaw/openclaw.json 4. 分析配置内容,检查: - - models.providers 中的服务商配置是否正确(baseUrl、apiKey 格式) - - gateway 配置(port、mode、auth)是否合理 - - 有没有常见的配置错误(比如 mode 放在了顶层) -5. 给出配置健康度评估和改进建议`, + - models.providers 服务商配置(baseUrl 格式、apiKey 是否存在) + - gateway 配置(port 默认 18789、mode 必须在 gateway 对象内) + - 常见配置错误(mode 放在顶层、缺少 gateway 对象、controlUi.allowedOrigins 未配置) +5. 给出配置健康度评估和具体改进建议`, }, { id: 'diagnose-gateway', - icon: '🏥', + icon: icon('shield', 16), name: '诊断 Gateway', desc: '检查 Gateway 运行状态、端口、日志', tools: ['terminal', 'fileOps'], prompt: `请帮我诊断 OpenClaw Gateway 的运行状态。 具体操作: -1. 先调用 get_system_info 获取操作系统类型,据此选择正确的命令 -2. 检查 Gateway 进程是否在运行(根据 OS 选择合适的进程查看命令) -3. 检查 Gateway 端口(默认 18789)是否在监听 -4. 读取最近的 Gateway 日志(.openclaw/logs/gateway.log,取最后 50 行) -5. 分析日志中是否有错误或警告 -6. 给出诊断结论和建议`, +1. 调用 get_system_info 获取 OS 类型和主目录 +2. 用 list_processes 工具检查 openclaw/gateway 进程是否在运行 +3. 用 check_port 工具检查端口 18789 是否在监听 +4. 用 read_file 读取 ~/.openclaw/logs/gateway.log(取最后 50 行) +5. 分析日志中的 ERROR、WARN、fail 等关键词 +6. 给出诊断结论(进程状态 + 端口状态 + 日志分析)和修复建议`, }, { id: 'browse-dir', - icon: '📂', + icon: icon('folder', 16), name: '浏览配置目录', desc: '查看 .openclaw 目录结构和文件', tools: ['fileOps'], prompt: `请帮我浏览 OpenClaw 的配置目录结构。 具体操作: -1. 先调用 get_system_info 获取主目录路径 -2. 用 list_directory 列出 .openclaw/ 根目录 -3. 列出 .openclaw/agents/ 下的 Agent 列表 -4. 对于默认 Agent(通常是 main),列出其 agent 子目录 -5. 简要说明每个目录/文件的作用 -6. 标注哪些是关键配置文件`, +1. 调用 get_system_info 获取主目录路径(Windows: $env:USERPROFILE, Mac/Linux: ~) +2. 用 list_directory 列出 ~/.openclaw/ 根目录 +3. 列出 ~/.openclaw/agents/ 下的 Agent 列表 +4. 对于 main Agent,列出 ~/.openclaw/agents/main/agent/ 子目录 +5. 简要说明每个目录/文件的作用: + - openclaw.json: 全局配置(模型、Gateway、工具) + - clawpanel.json: ClawPanel 面板配置 + - mcp.json: MCP 工具配置 + - agents/: Agent 工作目录 + - logs/: 日志文件 + - backups/: 配置备份 +6. 标注关键配置文件和常用路径`, }, { id: 'check-env', - icon: '💻', + icon: icon('monitor', 16), name: '检查系统环境', desc: '检测 Node.js、npm 版本和系统信息', tools: ['terminal'], prompt: `请帮我检查当前系统环境是否满足 OpenClaw 的运行要求。 具体操作: -1. 先调用 get_system_info 获取操作系统、架构等基础信息 -2. 根据 OS 类型执行对应命令检查 Node.js 版本(node -v),需要 >= 18 -3. 检查 npm 版本(npm -v) -4. 检查 OpenClaw 是否已安装及版本 -5. 检查磁盘空间 -6. 给出环境评估和缺失项提示`, +1. 调用 get_system_info 获取 OS、架构、Node.js 版本等基础信息 +2. 用 run_command 检查 Node.js 版本(node -v),要求 >= 18 +3. 用 run_command 检查 npm 版本(npm -v) +4. 用 run_command 检查 OpenClaw CLI(openclaw --version) +5. 用 check_port 检查 Gateway 端口 18789 +6. 给出环境评估报告,每项标注通过/失败,并给出缺失项的安装命令`, }, { id: 'analyze-logs', - icon: '📋', + icon: icon('clipboard', 16), name: '分析错误日志', desc: '读取最近日志,定位错误原因', tools: ['terminal', 'fileOps'], prompt: `请帮我分析 OpenClaw 最近的日志,找出可能的问题。 具体操作: -1. 先调用 get_system_info 获取系统信息和主目录 -2. 用 list_directory 查看 .openclaw/logs/ 有哪些日志文件 -3. 根据 OS 选择合适的命令读取 gateway.log 最后 100 行 -4. 搜索 ERROR、WARN、fail、exception 等关键词 -5. 如果有错误,分析原因并给出修复建议 -6. 汇总日志分析报告`, +1. 调用 get_system_info 获取主目录路径 +2. 用 list_directory 查看 ~/.openclaw/logs/ 有哪些日志文件 +3. 用 read_file 读取 ~/.openclaw/logs/gateway.log +4. 搜索 ERROR、WARN、fail、exception、SIGTERM、Bootstrap 等关键词 +5. 对照常见问题速查表分析错误原因 +6. 汇总日志分析报告,给出具体修复步骤`, }, { id: 'fix-common', - icon: '🔨', + icon: icon('wrench', 16), name: '一键排障', desc: '自动检测并修复常见问题', tools: ['terminal', 'fileOps'], prompt: `请帮我自动检测并修复 OpenClaw 的常见问题。 先调用 get_system_info 获取系统信息,然后按以下步骤逐一检查: -1. **配置检查**:读取 openclaw.json,检查是否有已知的配置错误(mode 在顶层、缺少 gateway 对象等) -2. **models.json 同步**:对比 openclaw.json 和 agents/main/agent/models.json 的 providers,检查是否同步 -3. **Gateway 状态**:根据 OS 选择合适命令,检查进程是否运行、端口是否监听 +1. **配置检查**:用 read_file 读取 openclaw.json,检查是否有已知错误(mode 在顶层、缺少 gateway 对象等) +2. **models.json 同步**:用 read_file 对比 openclaw.json 和 agents/main/agent/models.json 的 providers +3. **Gateway 状态**:用 list_processes 检查 openclaw 进程,用 check_port 检查端口 18789 4. **WebSocket 配置**:检查 gateway.controlUi.allowedOrigins 是否包含 "*" -5. **Node.js 环境**:检查 node 和 npm 是否可用 +5. **Node.js 环境**:用 run_command 检查 node 和 npm 版本 -对每个检查项给出 ✅ 或 ❌ 状态,并对发现的问题给出修复建议(但不要自动修改配置文件,等我确认)。`, +对每个检查项给出通过/失败状态,并对发现的问题给出具体修复命令(但不要自动修改配置文件,等我确认)。`, }, { id: 'report-bug', - icon: '🐛', + icon: icon('bug', 16), name: '提交 Bug 报告', desc: '整理问题信息,生成标准 Issue 提交到 GitHub', tools: ['terminal', 'fileOps'], prompt: `我想反馈一个 Bug,请帮我整理成标准的 GitHub Issue。 具体操作: -1. 先问我遇到了什么问题(如果我还没说的话) -2. 调用 get_system_info 获取我的系统环境信息 -3. 如果有日志,帮我读取最近的错误日志 -4. 自动收集环境信息:OS 版本、Node.js 版本、OpenClaw 版本、ClawPanel 版本 -5. 按照标准 Issue 模板整理好内容: +1. 用 ask_user 工具询问我遇到了什么问题(如果我还没说的话) +2. 调用 get_system_info 获取系统环境信息 +3. 用 run_command 收集:openclaw --version、node -v 等版本信息 +4. 用 read_file 读取最近的错误日志(如有) +5. 按标准 Issue 模板整理: - **问题描述**(一句话) - **复现步骤**(1, 2, 3...) - - **期望行为** - - **实际行为** + - **期望行为** / **实际行为** - **环境信息**(自动填充) - **相关日志**(如有) -6. 把整理好的 Issue 内容用代码块展示,并给出对应仓库的 Issue 链接 -7. 告诉我直接复制粘贴到 GitHub 即可提交`, +6. 用代码块展示完整 Issue 内容,给出对应仓库的 Issue 链接: + - ClawPanel: https://github.com/qingchencloud/clawpanel/issues/new + - OpenClaw: https://github.com/qingchencloud/openclaw-zh/issues/new + - cftunnel: https://github.com/qingchencloud/cftunnel/issues/new + - ClawApp: https://github.com/qingchencloud/clawapp/issues/new`, }, { id: 'pr-assistant', - icon: '🔀', + icon: icon('zap', 16), name: 'PR 助手', desc: '定位 Bug 原因,生成修复代码和 PR 描述', tools: ['terminal', 'fileOps'], @@ -539,6 +612,9 @@ function getEnabledTools() { // 终端工具:受设置开关控制(优先级高于模式) if (t.terminal !== false) tools.push(...TOOL_DEFS.terminal) + // 联网搜索工具:受设置开关控制 + if (t.webSearch !== false) tools.push(...TOOL_DEFS.webSearch) + // 文件工具:受设置开关控制 + 规划模式排除写入 if (t.fileOps !== false) { if (mode.readOnly) { @@ -643,7 +719,41 @@ function playModeTransition(page, modeKey) { } function buildSystemPrompt() { - let prompt = getSystemPromptBase() + let prompt = '' + + // 灵魂移植模式:用 OpenClaw Agent 的身份替代默认人设 + if (_config?.soulSource?.startsWith('openclaw:') && _soulCache) { + prompt += '# 你的身份\n' + if (_soulCache.identity) prompt += _soulCache.identity + '\n\n' + if (_soulCache.soul) prompt += '# 灵魂\n' + _soulCache.soul + '\n\n' + if (_soulCache.user) prompt += '# 你的用户\n' + _soulCache.user + '\n\n' + if (_soulCache.agents) { + // 截断 AGENTS.md 到约 4000 字符以节省 token + const agentsContent = _soulCache.agents.length > 4000 ? _soulCache.agents.slice(0, 4000) + '\n\n[...已截断]' : _soulCache.agents + prompt += '# 操作规则\n' + agentsContent + '\n\n' + } + if (_soulCache.tools) prompt += '# 工具笔记\n' + _soulCache.tools + '\n\n' + if (_soulCache.memory) { + const memContent = _soulCache.memory.length > 3000 ? _soulCache.memory.slice(-3000) : _soulCache.memory + prompt += '# 长期记忆\n' + memContent + '\n\n' + } + if (_soulCache.recentMemories?.length) { + prompt += '# 最近记忆\n' + for (const m of _soulCache.recentMemories) { + const content = m.content.length > 800 ? m.content.slice(0, 800) + '...' : m.content + prompt += `## ${m.date}\n${content}\n\n` + } + } + // 追加 ClawPanel 特有的产品知识和工具说明 + prompt += '\n# ClawPanel 工具能力\n你同时是 ClawPanel 内置助手,拥有以下额外能力:\n' + prompt += '- 执行终端命令、读写文件、浏览目录\n' + prompt += '- 联网搜索和网页抓取\n' + prompt += '- 管理 OpenClaw 配置和服务\n' + prompt += '- 你精通 OpenClaw 的架构、配置、Gateway、Agent 管理\n' + } else { + prompt += getSystemPromptBase() + } + const modeKey = currentMode() const mode = MODES[modeKey] @@ -709,9 +819,165 @@ function buildSystemPrompt() { } prompt += '\n\n当用户的需求匹配某个技能时,可以建议用户点击对应的技能卡片,或者你直接按技能的步骤操作。' + // 注入内置 OpenClaw 知识库 + prompt += '\n\n' + OPENCLAW_KB + + // 注入用户自定义知识库内容 + const kbEnabled = (_config.knowledgeFiles || []).filter(f => f.enabled !== false && f.content) + if (kbEnabled.length > 0) { + prompt += '\n\n## 用户自定义知识库' + prompt += '\n以下是用户提供的参考知识,回答问题时请优先参考这些内容:' + for (const kb of kbEnabled) { + const content = kb.content.length > 5000 ? kb.content.slice(0, 5000) + '\n\n[...内容已截断]' : kb.content + prompt += `\n\n### ${kb.name}\n${content}` + } + } + return prompt } +// ── 灵魂移植:扫描可用 Agent ── +async function scanOpenClawAgents() { + try { + const sysInfo = await api.assistantSystemInfo() + const home = sysInfo.match(/主目录[::]\s*(.+)/)?.[1]?.trim() || sysInfo.match(/Home[::]\s*(.+)/)?.[1]?.trim() || '' + if (!home) return [] + const agents = [] + // 默认主工作区始终存在于 ~/.openclaw/workspace + let defaultExists = false + try { await api.assistantListDir(home + '/.openclaw/workspace'); defaultExists = true } catch {} + agents.push({ id: 'default', label: '默认 (主工作区)', hasWorkspace: defaultExists }) + // 扫描自定义 Agent + try { + const agentsDir = home + '/.openclaw/agents' + const listing = await api.assistantListDir(agentsDir) + const dirs = listing.split('\n').filter(l => l.includes('[DIR]')) + .map(l => l.replace(/^\[DIR\]\s*/, '').replace(/[\/\\]+$/, '').trim()).filter(Boolean) + for (const id of dirs) { + if (id === 'main') continue // main 就是默认,已在上面添加 + const wsPath = agentsDir + '/' + id + '/workspace' + let hasWorkspace = false + try { await api.assistantListDir(wsPath); hasWorkspace = true } catch {} + agents.push({ id, label: id, hasWorkspace }) + } + } catch {} + return agents + } catch (err) { + console.error('[soul] 扫描 Agent 失败:', err) + return [] + } +} + +// ── 灵魂移植:加载指定 Agent 的身份 ── +async function loadOpenClawSoul(agentId = 'default') { + try { + const sysInfo = await api.assistantSystemInfo() + const home = sysInfo.match(/主目录[::]\s*(.+)/)?.[1]?.trim() || sysInfo.match(/Home[::]\s*(.+)/)?.[1]?.trim() || '' + if (!home) throw new Error('无法获取主目录') + // default/main 使用 ~/.openclaw/workspace,其他使用 agents/{id}/workspace + let ws + if (agentId === 'default' || agentId === 'main') { + ws = home + '/.openclaw/workspace' + } else { + ws = home + '/.openclaw/agents/' + agentId + '/workspace' + } + let wsExists = false + try { await api.assistantListDir(ws); wsExists = true } catch {} + if (!wsExists) throw new Error('Agent workspace 不存在: ' + agentId) + + const readSafe = async (p) => { try { return await api.assistantReadFile(p) } catch { return null } } + + const soul = { + agentId, + 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').map(l => l.trim()).filter(l => l.match(/\d{4}-\d{2}-\d{2}/)) + const recent = files.sort().slice(-3) + for (const f of recent) { + const fname = f.replace(/^\[FILE\]\s*/, '').replace(/\s*\(.*\)$/, '').trim() + const content = await readSafe(ws + '/memory/' + fname) + if (content) soul.recentMemories.push({ date: fname, content }) + } + } catch {} + + _soulCache = soul + return soul + } catch (err) { + console.error('[soul] 加载失败:', err) + _soulCache = null + return null + } +} + +// 获取灵魂文件的统计信息(用于 UI 显示) +function getSoulStats() { + if (!_soulCache) return [] + const files = [ + { name: 'SOUL.md', desc: '灵魂 · 人格边界', content: _soulCache.soul }, + { name: 'IDENTITY.md', desc: '身份 · 名称形象', content: _soulCache.identity }, + { name: 'USER.md', desc: '用户 · 偏好称呼', content: _soulCache.user }, + { name: 'AGENTS.md', desc: '规则 · 操作指令', content: _soulCache.agents }, + { name: 'TOOLS.md', desc: '笔记 · 工具环境', content: _soulCache.tools }, + { name: 'MEMORY.md', desc: '记忆 · 长期存储', content: _soulCache.memory }, + ] + return files.map(f => ({ + name: f.name, + desc: f.desc, + loaded: !!f.content, + size: f.content ? f.content.length : 0, + })) +} + +// 渲染灵魂文件加载状态卡片 +function renderSoulStats(soul) { + if (!soul) return '' + const stats = getSoulStats() + const loaded = stats.filter(f => f.loaded) + const totalSize = stats.reduce((s, f) => s + f.size, 0) + const memCount = soul.recentMemories?.length || 0 + const sizeStr = totalSize > 1024 ? (totalSize / 1024).toFixed(1) + ' KB' : totalSize + ' B' + + let html = `
+ + 已加载 ${loaded.length}/${stats.length} 个文件(${sizeStr}) +
` + + html += '
' + for (const f of stats) { + const fSize = f.loaded ? (f.size > 1024 ? (f.size / 1024).toFixed(1) + ' KB' : f.size + ' B') : '—' + html += `
+
${f.loaded ? '' : ''}
+
+ ${f.name} + ${f.desc} +
+ ${fSize} +
` + } + if (memCount > 0) { + html += `
+
+
+ memory/ + 每日记忆日志 +
+ ${memCount} 个文件 +
` + } + html += '
' + return html +} + // ── 状态 ── let _page = null, _messagesEl = null, _textarea = null, _sendBtn = null let _sessionListEl = null, _settingsPanel = null, _queueEl = null @@ -723,6 +989,8 @@ const _sessionStatus = new Map() // sessionId → 'idle' | 'streaming' | 'waitin let _messageQueue = [] // [{ id, text, ts }] let _streamRefreshTimer = null // 后台流式刷新定时器 let _pendingImages = [] // [{ id, dataUrl, name, size }] 待发送图片 +let _errorContext = null // 待处理的错误上下文 { scene, title, hint, error, ts } +let _soulCache = null // 灵魂移植缓存 { identity, soul, user, agents, tools, memory, recentMemories[] } // ── 节流保存 ── function throttledSave() { @@ -936,13 +1204,14 @@ function loadConfig() { _config = raw ? JSON.parse(raw) : null } catch { _config = null } if (!_config) { - _config = { baseUrl: '', apiKey: '', model: '', temperature: 0.7, tools: { terminal: false, fileOps: false }, assistantName: DEFAULT_NAME, assistantPersonality: DEFAULT_PERSONALITY } + _config = { baseUrl: '', apiKey: '', model: '', temperature: 0.7, tools: { terminal: false, fileOps: false, webSearch: false }, assistantName: DEFAULT_NAME, assistantPersonality: DEFAULT_PERSONALITY } } if (!_config.assistantName) _config.assistantName = DEFAULT_NAME if (!_config.assistantPersonality) _config.assistantPersonality = DEFAULT_PERSONALITY - if (!_config.tools) _config.tools = { terminal: false, fileOps: false } + if (!_config.tools) _config.tools = { terminal: false, fileOps: false, webSearch: false } if (!_config.mode) _config.mode = DEFAULT_MODE if (!_config.apiType) _config.apiType = 'openai' + if (!Array.isArray(_config.knowledgeFiles)) _config.knowledgeFiles = [] return _config } @@ -1010,7 +1279,7 @@ function autoTitle(session) { if (session.messages.length >= 1 && session.title === '新会话') { const firstUser = session.messages.find(m => m.role === 'user') if (firstUser) { - const txt = firstUser._text || (typeof firstUser.content === 'string' ? firstUser.content : (firstUser.content?.find?.(p => p.type === 'text')?.text || '📷 图片消息')) + const txt = firstUser._text || (typeof firstUser.content === 'string' ? firstUser.content : (firstUser.content?.find?.(p => p.type === 'text')?.text || '[图片消息]')) // 取第一行或前30字作为标题(跳过空行) const firstLine = txt.split('\n').find(l => l.trim()) || txt const title = firstLine.slice(0, 30) + (firstLine.length > 30 ? '...' : '') @@ -1183,7 +1452,7 @@ async function callChatCompletions(base, messages, onChunk) { reasoningChunks++ reasoningBuf += d.reasoning_content } - }) + }, _abortController?.signal) _lastDebugInfo.chunks = { total: chunkCount, content: contentChunks, reasoning: reasoningChunks } @@ -1247,7 +1516,7 @@ async function callResponsesAPI(base, messages, onChunk) { if (json.choices?.[0]?.delta?.content) { onChunk(json.choices[0].delta.content) } - }) + }, _abortController?.signal) } // ── Anthropic Messages API(/v1/messages)── @@ -1313,7 +1582,7 @@ async function callAnthropicMessages(base, messages, onChunk) { thinkingBuf += delta.thinking } } - }) + }, _abortController?.signal) _lastDebugInfo.chunks = { total: chunkCount, content: contentChunks, thinking: thinkingChunks } @@ -1372,45 +1641,62 @@ async function callGeminiGenerate(base, messages, onChunk) { chunkCount++ const text = json.candidates?.[0]?.content?.parts?.[0]?.text if (text) onChunk(text) - }) + }, _abortController?.signal) _lastDebugInfo.chunks = { total: chunkCount } } // ── 通用 SSE 流读取 ── -async function readSSEStream(resp, onEvent) { +async function readSSEStream(resp, onEvent, signal) { const reader = resp.body.getReader() const decoder = new TextDecoder() let buffer = '' - while (true) { - // chunk 超时:如果 30 秒内没有收到任何数据,视为超时 - const readPromise = reader.read() - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error('流式响应超时:30 秒内未收到数据')), TIMEOUT_CHUNK) - ) - const { done, value } = await Promise.race([readPromise, timeoutPromise]) - if (done) break + // 监听 abort 信号 → 取消 reader(关键:fetch abort 不会自动取消已建立的流) + const onAbort = () => { try { reader.cancel() } catch {} } + if (signal) { + if (signal.aborted) { reader.cancel(); throw new DOMException('Aborted', 'AbortError') } + signal.addEventListener('abort', onAbort, { once: true }) + } - buffer += decoder.decode(value, { stream: true }) - const lines = buffer.split('\n') - buffer = lines.pop() || '' + try { + while (true) { + if (signal?.aborted) throw new DOMException('Aborted', 'AbortError') - for (const line of lines) { - const trimmed = line.trim() - if (!trimmed) continue + // chunk 超时:如果 30 秒内没有收到任何数据,视为超时 + const readPromise = reader.read() + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('流式响应超时:30 秒内未收到数据')), TIMEOUT_CHUNK) + ) + const { done, value } = await Promise.race([readPromise, timeoutPromise]) + if (done) { + if (signal?.aborted) throw new DOMException('Aborted', 'AbortError') + break + } - // 处理 SSE event: 行 - if (trimmed.startsWith('event:')) continue + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' - if (!trimmed.startsWith('data:')) continue - const data = trimmed.slice(5).trim() - if (data === '[DONE]') return + for (const line of lines) { + if (signal?.aborted) throw new DOMException('Aborted', 'AbortError') + const trimmed = line.trim() + if (!trimmed) continue - try { - onEvent(JSON.parse(data)) - } catch {} + // 处理 SSE event: 行 + if (trimmed.startsWith('event:')) continue + + if (!trimmed.startsWith('data:')) continue + const data = trimmed.slice(5).trim() + if (data === '[DONE]') return + + try { + onEvent(JSON.parse(data)) + } catch {} + } } + } finally { + signal?.removeEventListener('abort', onAbort) } } @@ -1434,6 +1720,10 @@ async function executeTool(name, args) { return await api.assistantCheckPort(args.port) case 'ask_user': return await showAskUserCard(args) + case 'web_search': + return await api.assistantWebSearch(args.query, args.max_results) + case 'fetch_url': + return await api.assistantFetchUrl(args.url) default: return `未知工具: ${name}` } @@ -1500,7 +1790,7 @@ function showAskUserCard({ question, type, options, placeholder }) { // 替换卡片为已回答状态 card.innerHTML = `
${escHtml(question)}
-
✓ ${escHtml(answer)}
+
${icon('check', 14)} ${escHtml(answer)}
` card.classList.add('answered') @@ -1597,6 +1887,10 @@ async function callAIWithTools(messages, onStatus, onToolProgress) { const MAX_AUTO_ROUNDS = 8 for (let round = 0; ; round++) { + // 检查是否已被用户中止 + if (!_isStreaming || _abortController?.signal?.aborted) { + throw new DOMException('Aborted', 'AbortError') + } if (round >= MAX_AUTO_ROUNDS) { const answer = await showAskUserCard({ question: `AI 已连续调用工具 ${round} 轮,可能陷入循环。你希望怎么做?`, @@ -1810,7 +2104,7 @@ function renderToolBlocks(toolHistory) { // ask_user 工具不显示在工具块中(它有自己的交互卡片) if (tc.name === 'ask_user') return '' - const icon = { run_command: '⚡', write_file: '✏️', read_file: '📄', list_directory: '📂', get_system_info: '💻', list_processes: '📊', check_port: '🔌' }[tc.name] || '🔧' + const tcIcon = { run_command: icon('terminal', 14), write_file: icon('edit', 14), read_file: icon('file', 14), list_directory: icon('folder', 14), get_system_info: icon('monitor', 14), list_processes: icon('list', 14), check_port: icon('plug', 14) }[tc.name] || icon('wrench', 14) const label = { run_command: '执行命令', read_file: '读取文件', write_file: '写入文件', list_directory: '列出目录', get_system_info: '系统信息', list_processes: '进程列表', check_port: '端口检测' }[tc.name] || tc.name const argsStr = tc.name === 'run_command' ? escHtml(tc.args.command || '') : tc.name === 'read_file' ? escHtml(tc.args.path || '') @@ -1837,6 +2131,89 @@ function renderToolBlocks(toolHistory) { }).join('') } +// ── 错误上下文 Banner ── + +function checkErrorContext() { + const raw = sessionStorage.getItem('assistant-error-context') + if (!raw) return + try { + _errorContext = JSON.parse(raw) + // 不立即移除 sessionStorage,等用户操作后再移除 + } catch { _errorContext = null } +} + +function clearErrorContext() { + _errorContext = null + sessionStorage.removeItem('assistant-error-context') + _messagesEl?.querySelector('.ast-error-banner')?.remove() +} + +function renderErrorBanner() { + if (!_errorContext || !_messagesEl) return + // 避免重复 + if (_messagesEl.querySelector('.ast-error-banner')) return + + const ctx = _errorContext + const banner = document.createElement('div') + banner.className = 'ast-error-banner' + banner.innerHTML = ` +
+ ${statusIcon('warn', 18)} + ${escHtml(ctx.title)} +
+ + +
+
+ ${ctx.hint ? `
${escHtml(ctx.hint)}
` : ''} + ${ctx.error ? ` + +
+
${escHtml(ctx.error)}
+
+ ` : ''} + ` + + // 展开/折叠详细日志 + const toggleBtn = banner.querySelector('.ast-error-toggle') + const detailEl = banner.querySelector('.ast-error-banner-detail') + if (toggleBtn && detailEl) { + toggleBtn.addEventListener('click', () => { + const expanded = detailEl.classList.toggle('expanded') + toggleBtn.textContent = expanded ? '收起日志 ▲' : '查看详细日志 ▼' + }) + } + + // "让 AI 分析" → 组装 prompt 并发送 + banner.querySelector('.btn-analyze').addEventListener('click', () => { + const prompt = [ + ctx.scene ? `**场景**: ${ctx.scene}` : '', + ctx.title ? `**错误**: ${ctx.title}` : '', + ctx.hint ? `**提示**: ${ctx.hint}` : '', + ctx.error ? `\n\`\`\`\n${ctx.error}\n\`\`\`` : '', + '\n请分析以上错误信息,给出原因和修复方案。', + ].filter(Boolean).join('\n') + + // 自动切换到执行模式 + if (currentMode() === 'chat') { + _config.mode = 'execute' + saveConfig() + _page?.querySelectorAll('.ast-mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === 'execute')) + } + + clearErrorContext() + sendMessage(prompt) + }) + + // "忽略" → 移除 banner 和上下文 + banner.querySelector('.btn-dismiss').addEventListener('click', () => { + clearErrorContext() + }) + + // 插入到消息区域顶部 + _messagesEl.insertBefore(banner, _messagesEl.firstChild) +} + function renderMessages() { const session = getCurrentSession() if (!_messagesEl) return @@ -1861,9 +2238,12 @@ function renderMessages() {

${_config?.assistantName || DEFAULT_NAME}

我可以帮你分析日志、排查问题、配置 OpenClaw。
点击下方技能卡片,AI 会自动调用工具完成任务。

+ ${getAssistantGuideHtml()}
${skillCards}
` + // 在欢迎页也显示错误 banner + if (_errorContext) renderErrorBanner() return } @@ -1925,7 +2305,7 @@ function buildTestResult({ success, elapsed, usedApi, reqUrl, reqBody, respStatu } else if (success) { html += `✓ 模型回复成功 (${elapsed}ms, ${usedApi} API)` } else { - html += `⚠ HTTP ${respStatus} — 请求完成但未解析到回复内容` + html += `${statusIcon('warn', 14)} HTTP ${respStatus} — 请求完成但未解析到回复内容` } // 回复预览 if (reply) { @@ -1960,7 +2340,9 @@ function showSettings() { +
+