feat: v0.8.2 — 15 fixes + 4 features + 3 improvements
Fixes: - Stop force-appending /v1 to API URLs (breaks Volcengine /v3 etc) - SSH upgrade: --unset-all + --add for 4 git insteadOf rules - Feishu: builtin detection, overlay→modal fix, select field, plugin version persistence - Docker: HTML response detection, Web mode guidance - Chat: runId dedup prevents duplicate messages - Cron: RPC params name→id - Channels: Gateway reload async (instant UI response), toggle cache invalidation - Linux: auto sudo for non-root npm installs (libc geteuid) - Control UI: dynamic hostname + auth token for remote access - npm: mirror fallback (npmmirror→npmjs.org) - QQBot: native binding friendly error message - Error diagnosis: SSH vs Git-not-installed, native binding detection Features: - About page: company info (武汉晴辰天下网络科技有限公司) - model-presets.js: shared module for models.js + assistant.js - Feishu: dual plugin support (builtin vs official @larksuiteoapi) - Assistant: provider preset quick-fill buttons Improvements: - Website: dynamic download links from latest.json + claw.qt.cool proxy - Linux deploy docs: upgrade guide, Gitee mirror, sudo notes - linux-deploy.sh: Gitee fallback + sudo npm + mirror retry
33
CHANGELOG.md
@@ -5,6 +5,39 @@
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
||||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [0.8.2] - 2026-03-12
|
||||
|
||||
### 修复 (Fixes)
|
||||
|
||||
- **接口地址不再强制拼接 /v1** — 火山引擎(/v3)等第三方 API 不再被错误追加 /v1,仅 Ollama(端口 11434)自动补全
|
||||
- **OpenClaw 升级 SSH 失败** — 增加 `git://` 和 `git+ssh://` 协议重定向到 HTTPS,`--unset-all` + `--add` 确保 4 条规则全部生效
|
||||
- **飞书插件安装失败** — 新增内置插件检测(`is_plugin_builtin`),已内置时自动跳过 npm install
|
||||
- **飞书保存 ReferenceError** — 修复 `overlay is not defined`(应为 `modal`),修复表单收集不支持 `<select>` 字段
|
||||
- **飞书插件版本持久化** — 切换官方/内置插件后重新打开弹窗不再丢失选择,自动检测已安装的插件版本
|
||||
- **龙虾军团 Docker 检测报错** — 修复桌面版 Tauri 模式下返回 HTML 导致 JSON 解析失败,新增「需要 Web 部署模式」专属指引
|
||||
- **聊天重复消息** — 新增 runId 去重机制,防止 Gateway 多次触发同一消息产生重复气泡
|
||||
- **定时任务 RPC 参数** — `cron.remove` / `cron.run` / `cron.update` 参数从 `name` 修正为 `id`
|
||||
- **消息渠道操作响应慢** — `save` / `toggle` / `remove` 的 Gateway 重载改为后台异步执行,API 立即返回
|
||||
- **消息渠道 toggle 不刷新** — 扩展缓存失效范围至 `read_openclaw_config` + `read_platform_config`
|
||||
- **Linux 非 root 用户 sudo** — `npm_command()` 自动检测 `euid != 0` 并加 `sudo`
|
||||
- **Control UI 远程访问** — 动态使用浏览器域名/IP 替代硬编码 `127.0.0.1`,自动附带 Gateway auth token
|
||||
- **npm 镜像源降级重试** — 淘宝源安装失败时自动切换到官方源重试
|
||||
- **QQ 插件 native binding** — 检测到 OpenClaw CLI 原生依赖缺失时给出友好提示和修复命令
|
||||
- **错误诊断增强** — exit 128 区分 SSH/Git 未安装;新增 native binding 检测
|
||||
|
||||
### 新功能 (Features)
|
||||
|
||||
- **关于页面公司信息** — 新增「关于我们」板块:武汉晴辰天下网络科技有限公司
|
||||
- **模型预设共享模块** — 提取 `src/lib/model-presets.js`,消除 models.js 和 assistant.js 重复维护
|
||||
- **飞书双插件支持** — 内置插件(聊天入口)或飞书官方插件(操作文档/日历/任务)可选
|
||||
- **晴辰助手快捷选择** — 设置弹窗新增 OpenAI / DeepSeek / Ollama 等服务商一键填充按钮
|
||||
|
||||
### 改进 (Improvements)
|
||||
|
||||
- **官网下载链接动态化** — 从 `latest.json` 自动获取最新版本号,走 `claw.qt.cool/proxy/dl/` 国内代理
|
||||
- **Linux 部署文档完善** — 升级指南增加 Gitee 镜像、sudo 权限说明、淘宝源降级说明
|
||||
- **linux-deploy.sh** — Gitee clone fallback + sudo npm + 淘宝源 registry + 官方源降级
|
||||
|
||||
## [0.8.0] - 2026-03-12
|
||||
|
||||
### 新功能 (Features)
|
||||
|
||||
BIN
docs/00.png
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 180 KiB |
BIN
docs/01.png
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 261 KiB |
BIN
docs/02.png
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 394 KiB |
BIN
docs/03.png
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 178 KiB |
@@ -34,7 +34,7 @@
|
||||
"description": "OpenClaw AI Agent 可视化管理面板,基于 Tauri v2 的跨平台桌面应用。支持仪表盘监控、多模型配置、消息渠道管理、内置 QQ 机器人、实时 AI 聊天、记忆管理、Agent 管理、网关配置、内网穿透等功能。",
|
||||
"url": "https://claw.qt.cool/",
|
||||
"downloadUrl": "https://github.com/qingchencloud/clawpanel/releases/latest",
|
||||
"softwareVersion": "0.7.4",
|
||||
"softwareVersion": "0.8.2",
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": "晴辰云 QingchenCloud",
|
||||
@@ -1133,7 +1133,7 @@
|
||||
<div class="orb orb-2" style="top:auto;bottom:-100px"></div>
|
||||
<div class="container-sm" style="position:relative;z-index:10">
|
||||
<div class="section-header">
|
||||
<div class="reveal download-version"><span class="pulse"></span> v0.7.4 最新版</div>
|
||||
<div class="reveal download-version"><span class="pulse"></span> v0.8.2 最新版</div>
|
||||
<h2 class="reveal section-title"><span class="gradient-text">下载安装</span></h2>
|
||||
<p class="reveal section-desc">选择你的操作系统,一键下载安装</p>
|
||||
</div>
|
||||
@@ -1143,11 +1143,11 @@
|
||||
<h3>macOS</h3>
|
||||
<p class="dl-desc">支持 Apple Silicon 和 Intel 芯片</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.7.4_aarch64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.8.2_aarch64.dmg" target="_blank" rel="noopener">
|
||||
Apple Silicon (M1/M2/M3/M4)
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.7.4_x64.dmg" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.8.2_x64.dmg" target="_blank" rel="noopener">
|
||||
Intel 芯片
|
||||
<span class="dl-format">.dmg</span>
|
||||
</a>
|
||||
@@ -1165,11 +1165,11 @@
|
||||
<h3>Windows</h3>
|
||||
<p class="dl-desc">支持 Windows 10 及以上版本</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.7.4_x64-setup.exe" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.8.2_x64-setup.exe" target="_blank" rel="noopener">
|
||||
安装程序
|
||||
<span class="dl-format">.exe</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.7.4_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.8.2_x64_en-US.msi" target="_blank" rel="noopener">
|
||||
MSI 安装包
|
||||
<span class="dl-format">.msi</span>
|
||||
</a>
|
||||
@@ -1180,11 +1180,11 @@
|
||||
<h3>Linux</h3>
|
||||
<p class="dl-desc">支持主流 Linux 发行版</p>
|
||||
<div class="dl-links">
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.7.4_amd64.AppImage" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.8.2_amd64.AppImage" target="_blank" rel="noopener">
|
||||
通用版
|
||||
<span class="dl-format">.AppImage</span>
|
||||
</a>
|
||||
<a class="dl-link" href="https://github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.7.4_amd64.deb" target="_blank" rel="noopener">
|
||||
<a class="dl-link" href="https://claw.qt.cool/proxy/dl/github.com/qingchencloud/clawpanel/releases/latest/download/ClawPanel_0.8.2_amd64.deb" target="_blank" rel="noopener">
|
||||
Debian / Ubuntu
|
||||
<span class="dl-format">.deb</span>
|
||||
</a>
|
||||
@@ -1574,6 +1574,27 @@
|
||||
if (target) setTimeout(function() { smoothScrollTo(target); }, 100);
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Dynamic Download Links from latest.json ── */
|
||||
(function() {
|
||||
var BASE = 'https://claw.qt.cool/update/latest.json';
|
||||
fetch(BASE).then(function(r) { return r.json(); }).then(function(d) {
|
||||
var ver = d.version;
|
||||
if (!ver) return;
|
||||
// 更新版本徽章
|
||||
var badge = document.querySelector('.download-version');
|
||||
if (badge) badge.innerHTML = '<span class="pulse"></span> v' + ver + ' 最新版';
|
||||
// 更新 JSON-LD 版本
|
||||
// 更新所有下载链接:替换旧版本号为新版本号
|
||||
document.querySelectorAll('#download .dl-link').forEach(function(a) {
|
||||
var href = a.getAttribute('href');
|
||||
if (!href) return;
|
||||
// 替换 URL 中的版本号模式 ClawPanel_X.Y.Z_
|
||||
var updated = href.replace(/ClawPanel_[\d.]+_/g, 'ClawPanel_' + ver + '_');
|
||||
a.setAttribute('href', updated);
|
||||
});
|
||||
}).catch(function() { /* 静默失败,保留硬编码链接作为兜底 */ });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -307,19 +307,43 @@ sudo firewall-cmd --reload
|
||||
### 更新 ClawPanel
|
||||
|
||||
```bash
|
||||
cd /opt/clawpanel
|
||||
cd /opt/clawpanel # root 部署路径
|
||||
# 或 ~/.local/share/clawpanel # 普通用户路径
|
||||
|
||||
git pull origin main
|
||||
npm install
|
||||
npm install --registry https://registry.npmmirror.com
|
||||
sudo systemctl restart clawpanel # 或 pm2 restart clawpanel
|
||||
```
|
||||
|
||||
> 国内拉不到 GitHub?用 Gitee 镜像:
|
||||
> ```bash
|
||||
> git remote set-url origin https://gitee.com/QtCodeCreators/clawpanel.git
|
||||
> git pull origin main
|
||||
> ```
|
||||
|
||||
### 更新 OpenClaw
|
||||
|
||||
**方式一:在 ClawPanel 面板中操作**(推荐)
|
||||
|
||||
打开「关于」页面 → 点击「检查更新」→ 按提示升级。面板会自动处理 sudo 权限和镜像源。
|
||||
|
||||
**方式二:命令行手动升级**
|
||||
|
||||
```bash
|
||||
npm install -g @qingchencloud/openclaw-zh@latest --registry https://registry.npmmirror.com
|
||||
# 非 root 用户需要 sudo
|
||||
sudo npm install -g @qingchencloud/openclaw-zh@latest --registry https://registry.npmmirror.com
|
||||
|
||||
# 淘宝源安装失败?换官方源重试(可能需要翻墙或代理)
|
||||
sudo npm install -g @qingchencloud/openclaw-zh@latest --registry https://registry.npmjs.org
|
||||
```
|
||||
|
||||
或在 ClawPanel 面板中点击「检查更新」按钮。
|
||||
> **权限说明**:Linux 全局 npm 包安装需要 root 权限。ClawPanel v0.8.1+ 已自动检测非 root 用户并加 sudo。如仍遇权限问题,手动加 `sudo` 即可。
|
||||
|
||||
### 更新频率
|
||||
|
||||
- **ClawPanel**:`git pull` 获取最新代码,无需重新安装依赖(除非 package.json 变了)
|
||||
- **OpenClaw**:通过 npm 全局升级,面板会自动检测新版本并提示
|
||||
- **前端热更新**:面板支持前端热更新(不需要 git pull),在「关于」页面点击「热更新」按钮即可
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clawpanel",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
|
||||
"type": "module",
|
||||
|
||||
@@ -9,6 +9,7 @@ echo ""
|
||||
|
||||
PANEL_PORT=1420
|
||||
REPO_URL="https://github.com/qingchencloud/clawpanel.git"
|
||||
REPO_URL_GITEE="https://gitee.com/QtCodeCreators/clawpanel.git"
|
||||
NPM_REGISTRY="https://registry.npmmirror.com"
|
||||
|
||||
# 检测权限模式
|
||||
@@ -115,7 +116,13 @@ install_openclaw() {
|
||||
echo "✅ OpenClaw 已安装: $(openclaw --version 2>/dev/null || echo '未知版本')"
|
||||
else
|
||||
echo "📦 安装 OpenClaw 汉化版..."
|
||||
npm install -g @qingchencloud/openclaw-zh --registry "$NPM_REGISTRY"
|
||||
if [ "$IS_ROOT" = true ]; then
|
||||
npm install -g @qingchencloud/openclaw-zh --registry "$NPM_REGISTRY" || \
|
||||
npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmjs.org
|
||||
else
|
||||
sudo npm install -g @qingchencloud/openclaw-zh --registry "$NPM_REGISTRY" || \
|
||||
sudo npm install -g @qingchencloud/openclaw-zh --registry https://registry.npmjs.org
|
||||
fi
|
||||
echo "✅ OpenClaw 安装完成"
|
||||
fi
|
||||
|
||||
@@ -132,13 +139,16 @@ install_clawpanel() {
|
||||
echo "📦 ClawPanel 已存在,更新中..."
|
||||
cd "$INSTALL_DIR"
|
||||
git pull origin main 2>/dev/null || true
|
||||
npm install
|
||||
npm install --registry "$NPM_REGISTRY"
|
||||
else
|
||||
echo "📦 克隆 ClawPanel..."
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
if ! git clone "$REPO_URL" "$INSTALL_DIR" 2>/dev/null; then
|
||||
echo "⚠️ GitHub 克隆失败,切换到 Gitee 国内镜像..."
|
||||
git clone "$REPO_URL_GITEE" "$INSTALL_DIR"
|
||||
fi
|
||||
cd "$INSTALL_DIR"
|
||||
npm install
|
||||
npm install --registry "$NPM_REGISTRY"
|
||||
fi
|
||||
# 生产构建(生成优化后的静态文件)
|
||||
echo "📦 构建生产版本..."
|
||||
|
||||
3
src-tauri/Cargo.lock
generated
@@ -328,12 +328,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clawpanel"
|
||||
version = "0.8.0"
|
||||
version = "0.8.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"dirs",
|
||||
"ed25519-dalek",
|
||||
"libc",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "clawpanel"
|
||||
version = "0.8.0"
|
||||
version = "0.8.2"
|
||||
edition = "2021"
|
||||
description = "ClawPanel - OpenClaw 可视化管理面板"
|
||||
authors = ["qingchencloud"]
|
||||
@@ -31,3 +31,4 @@ base64 = "0.22"
|
||||
urlencoding = "2"
|
||||
regex = "1"
|
||||
tokio = { version = "1", features = ["process", "time"] }
|
||||
libc = "0.2"
|
||||
|
||||
@@ -42,6 +42,7 @@ fn get_configured_registry() -> String {
|
||||
|
||||
/// 创建使用配置源的 npm Command
|
||||
/// Windows 上 npm 是 npm.cmd,需要通过 cmd /c 调用,并隐藏窗口
|
||||
/// Linux 非 root 用户全局安装需要 sudo
|
||||
fn npm_command() -> Command {
|
||||
let registry = get_configured_registry();
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -53,13 +54,29 @@ fn npm_command() -> Command {
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
cmd
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut cmd = Command::new("npm");
|
||||
cmd.args(["--registry", ®istry]);
|
||||
cmd.env("PATH", super::enhanced_path());
|
||||
cmd
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Linux 非 root 用户全局 npm install 需要 sudo
|
||||
let need_sudo = unsafe { libc::geteuid() } != 0;
|
||||
let mut cmd = if need_sudo {
|
||||
let mut c = Command::new("sudo");
|
||||
c.args(["npm", "--registry", ®istry]);
|
||||
c
|
||||
} else {
|
||||
let mut c = Command::new("npm");
|
||||
c.args(["--registry", ®istry]);
|
||||
c
|
||||
};
|
||||
cmd.env("PATH", super::enhanced_path());
|
||||
cmd
|
||||
}
|
||||
}
|
||||
|
||||
fn backups_dir() -> PathBuf {
|
||||
@@ -611,24 +628,28 @@ pub async fn upgrade_openclaw(
|
||||
let old_pkg = npm_package_name(¤t_source);
|
||||
let need_uninstall_old = current_source != source;
|
||||
|
||||
// 自动配置 git 使用 HTTPS 替代 SSH,避免用户没配 SSH Key 导致依赖安装失败
|
||||
// 自动配置 git 全面使用 HTTPS 替代 SSH/git 协议,避免用户没配 SSH Key 导致依赖安装失败
|
||||
let _ = app.emit("upgrade-log", "配置 Git HTTPS 模式...");
|
||||
// 先清除旧的 insteadOf 规则,再逐条添加(git config 不带 --add 会覆盖,只保留最后一条)
|
||||
let _ = Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"--global",
|
||||
"url.https://github.com/.insteadOf",
|
||||
"ssh://git@github.com/",
|
||||
])
|
||||
.output();
|
||||
let _ = Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"--global",
|
||||
"url.https://github.com/.insteadOf",
|
||||
"git@github.com:",
|
||||
])
|
||||
.args(["config", "--global", "--unset-all", "url.https://github.com/.insteadOf"])
|
||||
.output();
|
||||
for from in &[
|
||||
"ssh://git@github.com/",
|
||||
"git@github.com:",
|
||||
"git://github.com/",
|
||||
"git+ssh://git@github.com/",
|
||||
] {
|
||||
let _ = Command::new("git")
|
||||
.args([
|
||||
"config",
|
||||
"--global",
|
||||
"--add",
|
||||
"url.https://github.com/.insteadOf",
|
||||
from,
|
||||
])
|
||||
.output();
|
||||
}
|
||||
|
||||
let _ = app.emit("upgrade-log", format!("$ npm install -g {pkg}"));
|
||||
let _ = app.emit("upgrade-progress", 10);
|
||||
@@ -651,6 +672,8 @@ pub async fn upgrade_openclaw(
|
||||
|
||||
let mut child = npm_command()
|
||||
.args(["install", "-g", &pkg, "--registry", registry, "--verbose"])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes -o StrictHostKeyChecking=no")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
@@ -695,20 +718,66 @@ pub async fn upgrade_openclaw(
|
||||
.code()
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or("unknown".into());
|
||||
let _ = app.emit("upgrade-log", format!("❌ 升级失败 (exit code: {code})"));
|
||||
// 把 stderr 最后 15 行带进错误消息,确保前端诊断函数能匹配到
|
||||
// npm 内部错误码(如 -4058 ENOENT、EPERM 等)
|
||||
let tail = stderr_lines
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.rev()
|
||||
.take(15)
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
return Err(format!("升级失败,exit code: {code}\n{tail}"));
|
||||
|
||||
// 如果使用了镜像源失败,自动降级到官方源重试
|
||||
let used_mirror = registry.contains("npmmirror.com") || registry.contains("taobao.org");
|
||||
if used_mirror {
|
||||
let _ = app.emit("upgrade-log", "");
|
||||
let _ = app.emit("upgrade-log", "⚠️ 镜像源安装失败,自动切换到官方源重试...");
|
||||
let _ = app.emit("upgrade-progress", 15);
|
||||
let fallback = "https://registry.npmjs.org";
|
||||
let mut child2 = npm_command()
|
||||
.args(["install", "-g", &pkg, "--registry", fallback, "--verbose"])
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.env("GIT_SSH_COMMAND", "ssh -o BatchMode=yes -o StrictHostKeyChecking=no")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("执行重试命令失败: {e}"))?;
|
||||
let stderr2 = child2.stderr.take();
|
||||
let stdout2 = child2.stdout.take();
|
||||
let app3 = app.clone();
|
||||
let stderr_lines3 = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let stderr_lines4 = stderr_lines3.clone();
|
||||
let handle2 = std::thread::spawn(move || {
|
||||
if let Some(pipe) = stderr2 {
|
||||
let mut p: u32 = 20;
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app3.emit("upgrade-log", &line);
|
||||
stderr_lines4.lock().unwrap().push(line);
|
||||
if p < 75 { p += 2; let _ = app3.emit("upgrade-progress", p); }
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(pipe) = stdout2 {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app.emit("upgrade-log", &line);
|
||||
}
|
||||
}
|
||||
let _ = handle2.join();
|
||||
let _ = app.emit("upgrade-progress", 80);
|
||||
let status2 = child2.wait().map_err(|e| format!("等待重试进程失败: {e}"))?;
|
||||
let _ = app.emit("upgrade-progress", 100);
|
||||
if !status2.success() {
|
||||
let code2 = status2.code().map(|c| c.to_string()).unwrap_or("unknown".into());
|
||||
let tail = stderr_lines3.lock().unwrap().iter().rev().take(15).rev().cloned().collect::<Vec<_>>().join("\n");
|
||||
return Err(format!("升级失败(镜像源和官方源均失败),exit code: {code2}\n{tail}"));
|
||||
}
|
||||
let _ = app.emit("upgrade-log", "✅ 官方源安装成功");
|
||||
} else {
|
||||
let _ = app.emit("upgrade-log", format!("❌ 升级失败 (exit code: {code})"));
|
||||
let tail = stderr_lines
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.rev()
|
||||
.take(15)
|
||||
.rev()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
return Err(format!("升级失败,exit code: {code}\n{tail}"));
|
||||
}
|
||||
}
|
||||
|
||||
// 安装成功后再卸载旧包(确保 CLI 始终可用)
|
||||
@@ -1307,13 +1376,8 @@ fn normalize_base_url_for_api(raw: &str, api_type: &str) -> String {
|
||||
}
|
||||
"google-gemini" => base,
|
||||
_ => {
|
||||
if !base.ends_with("/v1") {
|
||||
if let Some(idx) = base.find("/v1/") {
|
||||
base.truncate(idx + 3);
|
||||
} else {
|
||||
base.push_str("/v1");
|
||||
}
|
||||
}
|
||||
// 不再强制追加 /v1,尊重用户填写的 URL(火山引擎等第三方用 /v3 等路径)
|
||||
// 仅 Ollama (端口 11434) 自动补 /v1
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
@@ -416,8 +416,11 @@ pub async fn save_messaging_platform(
|
||||
// 写回配置并重载 Gateway
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
|
||||
// 触发 Gateway 重载使配置生效
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
// Gateway 重载在后台进行,不阻塞 UI 响应
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = super::config::do_reload_gateway(&app2).await;
|
||||
});
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
@@ -436,7 +439,11 @@ pub async fn remove_messaging_platform(
|
||||
}
|
||||
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
// Gateway 重载在后台进行,不阻塞 UI 响应
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = super::config::do_reload_gateway(&app2).await;
|
||||
});
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
@@ -462,7 +469,11 @@ pub async fn toggle_messaging_platform(
|
||||
}
|
||||
|
||||
super::config::save_openclaw_json(&cfg)?;
|
||||
let _ = super::config::do_reload_gateway(&app).await;
|
||||
// Gateway 重载在后台进行,不阻塞 UI 响应
|
||||
let app2 = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _ = super::config::do_reload_gateway(&app2).await;
|
||||
});
|
||||
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
@@ -519,6 +530,9 @@ pub async fn get_channel_plugin_status(plugin_id: String) -> Result<Value, Strin
|
||||
let installed = plugin_dir.is_dir() && plugin_install_marker_exists(&plugin_dir);
|
||||
let legacy_backup_detected = legacy_plugin_backup_dir(plugin_id).exists();
|
||||
|
||||
// 检测插件是否为 OpenClaw 内置(新版 openclaw/openclaw-zh 打包了 feishu 等插件)
|
||||
let builtin = is_plugin_builtin(plugin_id);
|
||||
|
||||
let cfg = super::config::load_openclaw_json().unwrap_or_else(|_| json!({}));
|
||||
let allowed = cfg
|
||||
.get("plugins")
|
||||
@@ -536,6 +550,7 @@ pub async fn get_channel_plugin_status(plugin_id: String) -> Result<Value, Strin
|
||||
|
||||
Ok(json!({
|
||||
"installed": installed,
|
||||
"builtin": builtin,
|
||||
"path": plugin_dir.to_string_lossy(),
|
||||
"allowed": allowed,
|
||||
"enabled": enabled,
|
||||
@@ -881,6 +896,50 @@ fn cleanup_failed_qqbot_install(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检测插件是否为 OpenClaw 内置(作为 npm 依赖打包在 openclaw/openclaw-zh 中)
|
||||
fn is_plugin_builtin(plugin_id: &str) -> bool {
|
||||
// 插件 ID → npm 包名映射
|
||||
let pkg_name = match plugin_id {
|
||||
"feishu" => "@openclaw/feishu",
|
||||
"dingtalk-connector" => "@dingtalk-real-ai/dingtalk-connector",
|
||||
_ => return false,
|
||||
};
|
||||
// 在全局 npm node_modules 中查找 openclaw 安装目录
|
||||
let npm_dirs: Vec<PathBuf> = {
|
||||
let mut dirs = Vec::new();
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(appdata) = std::env::var_os("APPDATA") {
|
||||
let base = PathBuf::from(appdata).join("npm").join("node_modules");
|
||||
dirs.push(base.join("@qingchencloud").join("openclaw-zh"));
|
||||
dirs.push(base.join("openclaw"));
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
dirs.push(PathBuf::from("/opt/homebrew/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/opt/homebrew/lib/node_modules/openclaw"));
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/openclaw"));
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/usr/local/lib/node_modules/openclaw"));
|
||||
dirs.push(PathBuf::from("/usr/lib/node_modules/@qingchencloud/openclaw-zh"));
|
||||
dirs.push(PathBuf::from("/usr/lib/node_modules/openclaw"));
|
||||
}
|
||||
dirs
|
||||
};
|
||||
// 插件包名拆分成路径片段,如 @openclaw/feishu → @openclaw/feishu
|
||||
let pkg_path: PathBuf = pkg_name.split('/').collect();
|
||||
for base in &npm_dirs {
|
||||
let candidate = base.join("node_modules").join(&pkg_path);
|
||||
if candidate.join("package.json").is_file() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn generic_plugin_dir(plugin_id: &str) -> PathBuf {
|
||||
super::openclaw_dir().join("extensions").join(plugin_id)
|
||||
}
|
||||
@@ -1117,10 +1176,13 @@ pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result<String, Strin
|
||||
|
||||
let stderr = child.stderr.take();
|
||||
let app2 = app.clone();
|
||||
let qqbot_stderr_lines = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
|
||||
let qqbot_stderr_clone = qqbot_stderr_lines.clone();
|
||||
let handle = std::thread::spawn(move || {
|
||||
if let Some(pipe) = stderr {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app2.emit("plugin-log", &line);
|
||||
qqbot_stderr_clone.lock().unwrap().push(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1128,9 +1190,11 @@ pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result<String, Strin
|
||||
let _ = app.emit("plugin-progress", 30);
|
||||
|
||||
let mut progress = 30;
|
||||
let mut qqbot_stdout_lines = Vec::new();
|
||||
if let Some(pipe) = child.stdout.take() {
|
||||
for line in BufReader::new(pipe).lines().map_while(Result::ok) {
|
||||
let _ = app.emit("plugin-log", &line);
|
||||
qqbot_stdout_lines.push(line);
|
||||
if progress < 90 {
|
||||
progress += 10;
|
||||
let _ = app.emit("plugin-progress", progress);
|
||||
@@ -1145,6 +1209,26 @@ pub async fn install_qqbot_plugin(app: tauri::AppHandle) -> Result<String, Strin
|
||||
.wait()
|
||||
.map_err(|e| format!("等待安装进程失败: {}", e))?;
|
||||
|
||||
// 检测 native binding 缺失(macOS/Linux 上 OpenClaw CLI 自身启动失败)
|
||||
let all_output = {
|
||||
let stderr_guard = qqbot_stderr_lines.lock().unwrap();
|
||||
let mut combined = qqbot_stdout_lines.join("\n");
|
||||
combined.push('\n');
|
||||
combined.push_str(&stderr_guard.join("\n"));
|
||||
combined
|
||||
};
|
||||
if all_output.contains("native binding") || all_output.contains("Failed to start CLI") {
|
||||
let _ = app.emit("plugin-log", "");
|
||||
let _ = app.emit("plugin-log", "⚠️ 检测到 OpenClaw CLI 原生依赖问题(native binding 缺失)");
|
||||
let _ = app.emit("plugin-log", "这是 OpenClaw 的上游依赖问题,非 QQBot 插件本身的问题。");
|
||||
let _ = app.emit("plugin-log", "请在终端手动执行以下命令重装 OpenClaw:");
|
||||
let _ = app.emit("plugin-log", " npm i -g @qingchencloud/openclaw-zh@latest --registry https://registry.npmmirror.com");
|
||||
let _ = app.emit("plugin-log", "重装完成后再回来安装 QQBot 插件。");
|
||||
let _ = cleanup_failed_qqbot_install(had_existing_plugin, had_existing_config);
|
||||
let _ = app.emit("plugin-progress", 100);
|
||||
return Err("OpenClaw CLI 原生依赖缺失,请先在终端重装 OpenClaw(详见上方日志)".into());
|
||||
}
|
||||
|
||||
let finalize = (|| -> Result<(), String> {
|
||||
if !status.success() {
|
||||
let _ = app.emit(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "ClawPanel",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.2",
|
||||
"identifier": "ai.openclaw.clawpanel",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -15,20 +15,36 @@ export function diagnoseInstallError(errStr) {
|
||||
// ===== 1. Git 相关 =====
|
||||
|
||||
// git SSH 权限问题(有 git 但没配 SSH Key)
|
||||
if (s.includes('permission denied (publickey)') || s.includes('ssh://git@github')) {
|
||||
if (s.includes('permission denied (publickey)') || s.includes('ssh://git@github') || s.includes('git@github.com')) {
|
||||
return {
|
||||
title: '安装失败 — Git SSH 权限',
|
||||
hint: '依赖包用了 SSH 协议拉取代码,但你没配 GitHub SSH Key。运行以下命令改用 HTTPS:',
|
||||
command: 'git config --global url."https://github.com/".insteadOf ssh://git@github.com/',
|
||||
command: 'git config --global url."https://github.com/".insteadOf ssh://git@github.com/ && git config --global --add url."https://github.com/".insteadOf git@github.com: && git config --global --add url."https://github.com/".insteadOf git://github.com/',
|
||||
}
|
||||
}
|
||||
|
||||
// git 未安装(exit 128 + access rights)
|
||||
if (s.includes('code 128') || s.includes('exit 128') || s.includes('access rights')) {
|
||||
// git exit 128:优先判断是 SSH 失败还是 Git 未安装
|
||||
if (s.includes('code 128') || s.includes('exit 128')) {
|
||||
if (s.includes('ssh') || s.includes('git@') || s.includes('publickey') || s.includes('access rights')) {
|
||||
return {
|
||||
title: '安装失败 — Git SSH 权限',
|
||||
hint: '依赖包用了 SSH 协议拉取代码,但你没配 GitHub SSH Key。运行以下命令改用 HTTPS:',
|
||||
command: 'git config --global url."https://github.com/".insteadOf ssh://git@github.com/ && git config --global --add url."https://github.com/".insteadOf git@github.com: && git config --global --add url."https://github.com/".insteadOf git://github.com/',
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: '安装失败 — 需要安装 Git',
|
||||
hint: '部分依赖需要通过 Git 下载。请先安装 Git 后重试。',
|
||||
command: '下载 Git: https://git-scm.com/downloads',
|
||||
title: '安装失败 — Git 错误',
|
||||
hint: 'Git 操作返回错误(exit 128)。可能是 Git 未安装,或 SSH 认证失败。请确认 Git 已安装,或手动执行以下命令切换到 HTTPS 模式:',
|
||||
command: 'git config --global url."https://github.com/".insteadOf ssh://git@github.com/ && git config --global --add url."https://github.com/".insteadOf git@github.com:',
|
||||
}
|
||||
}
|
||||
|
||||
// native binding 缺失(macOS/Linux 上 OpenClaw 的原生依赖问题)
|
||||
if (s.includes('cannot find native binding') || s.includes('native binding')) {
|
||||
return {
|
||||
title: '安装失败 — 原生依赖缺失',
|
||||
hint: 'OpenClaw 的原生模块未正确安装。这通常是 npm optional dependencies 的 bug。请尝试在终端手动重装:',
|
||||
command: 'npm i -g @qingchencloud/openclaw-zh@latest --registry https://registry.npmmirror.com',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
83
src/lib/model-presets.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 共享模型预设配置
|
||||
* models.js 和 assistant.js 共用,只需维护一套数据
|
||||
*/
|
||||
|
||||
// API 接口类型选项
|
||||
export const API_TYPES = [
|
||||
{ value: 'openai-completions', label: 'OpenAI 兼容 (最常用)' },
|
||||
{ value: 'anthropic-messages', label: 'Anthropic 原生' },
|
||||
{ value: 'openai-responses', label: 'OpenAI Responses' },
|
||||
{ value: 'google-gemini', label: 'Google Gemini' },
|
||||
]
|
||||
|
||||
// 服务商快捷预设
|
||||
export const PROVIDER_PRESETS = [
|
||||
{ key: 'openai', label: 'OpenAI 官方', baseUrl: 'https://api.openai.com/v1', api: 'openai-completions' },
|
||||
{ key: 'anthropic', label: 'Anthropic 官方', baseUrl: 'https://api.anthropic.com', api: 'anthropic-messages' },
|
||||
{ key: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', api: 'openai-completions' },
|
||||
{ key: 'google', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', api: 'google-gemini' },
|
||||
{ key: 'ollama', label: 'Ollama (本地)', baseUrl: 'http://127.0.0.1:11434/v1', api: 'openai-completions' },
|
||||
]
|
||||
|
||||
// gpt.qt.cool 推广配置
|
||||
export const QTCOOL = {
|
||||
baseUrl: 'https://gpt.qt.cool/v1',
|
||||
defaultKey: 'sk-0JDu7hyc51ZKD4iNebpFu07EUEhXmVVc',
|
||||
site: 'https://gpt.qt.cool/',
|
||||
usageUrl: 'https://gpt.qt.cool/user?key=',
|
||||
providerKey: 'qtcool',
|
||||
api: 'openai-completions',
|
||||
models: [] // 始终从 API 动态获取最新模型列表
|
||||
}
|
||||
|
||||
// 常用模型预设(按服务商分组)
|
||||
export const MODEL_PRESETS = {
|
||||
openai: [
|
||||
{ id: 'gpt-4o', name: 'GPT-4o', contextWindow: 128000 },
|
||||
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', contextWindow: 128000 },
|
||||
{ id: 'o3-mini', name: 'o3 Mini', contextWindow: 200000, reasoning: true },
|
||||
],
|
||||
anthropic: [
|
||||
{ id: 'claude-sonnet-4-5-20250514', name: 'Claude Sonnet 4.5', contextWindow: 200000 },
|
||||
{ id: 'claude-haiku-3-5-20241022', name: 'Claude Haiku 3.5', contextWindow: 200000 },
|
||||
],
|
||||
deepseek: [
|
||||
{ id: 'deepseek-chat', name: 'DeepSeek V3', contextWindow: 64000 },
|
||||
{ id: 'deepseek-reasoner', name: 'DeepSeek R1', contextWindow: 64000, reasoning: true },
|
||||
],
|
||||
google: [
|
||||
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', contextWindow: 1000000, reasoning: true },
|
||||
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', contextWindow: 1000000 },
|
||||
],
|
||||
ollama: [
|
||||
{ id: 'qwen2.5:7b', name: 'Qwen 2.5 7B', contextWindow: 32768 },
|
||||
{ id: 'llama3.2', name: 'Llama 3.2', contextWindow: 8192 },
|
||||
{ id: 'gemma3', name: 'Gemma 3', contextWindow: 32768 },
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* 动态获取 QTCOOL 模型列表
|
||||
* @param {string} [apiKey] - 自定义密钥,不传则用默认密钥
|
||||
* @returns {Promise<Array<{id:string, name:string, contextWindow:number, reasoning?:boolean}>>}
|
||||
*/
|
||||
export async function fetchQtcoolModels(apiKey) {
|
||||
const key = apiKey || QTCOOL.defaultKey
|
||||
try {
|
||||
const resp = await fetch(QTCOOL.baseUrl + '/models', {
|
||||
headers: { 'Authorization': 'Bearer ' + key },
|
||||
signal: AbortSignal.timeout(8000)
|
||||
})
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
if (data.data && data.data.length) {
|
||||
return data.data.map(m => ({
|
||||
id: m.id, name: m.id, contextWindow: 128000,
|
||||
reasoning: m.id.includes('codex')
|
||||
})).sort((a, b) => b.id.localeCompare(a.id))
|
||||
}
|
||||
}
|
||||
} catch { /* use fallback */ }
|
||||
return QTCOOL.models
|
||||
}
|
||||
@@ -107,6 +107,11 @@ async function webInvoke(cmd, args) {
|
||||
if (!isTauri && window.__clawpanel_show_login) window.__clawpanel_show_login()
|
||||
throw new Error('需要登录')
|
||||
}
|
||||
// 检测后端是否可用:如果返回的是 HTML(非 JSON),说明后端未运行
|
||||
const ct = (resp.headers.get('content-type') || '').toLowerCase()
|
||||
if (ct.includes('text/html') || ct.includes('text/plain')) {
|
||||
throw new Error('后端服务未运行,该功能需要 Web 部署模式')
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({ error: `HTTP ${resp.status}` }))
|
||||
throw new Error(data.error || `HTTP ${resp.status}`)
|
||||
@@ -196,7 +201,7 @@ export const api = {
|
||||
readPlatformConfig: (platform) => invoke('read_platform_config', { platform }),
|
||||
saveMessagingPlatform: (platform, form) => { invalidate('list_configured_platforms', 'read_platform_config'); return invoke('save_messaging_platform', { platform, form }) },
|
||||
removeMessagingPlatform: (platform) => { invalidate('list_configured_platforms', 'read_platform_config'); return invoke('remove_messaging_platform', { platform }) },
|
||||
toggleMessagingPlatform: (platform, enabled) => { invalidate('list_configured_platforms'); return invoke('toggle_messaging_platform', { platform, enabled }) },
|
||||
toggleMessagingPlatform: (platform, enabled) => { invalidate('list_configured_platforms', 'read_openclaw_config', 'read_platform_config'); return invoke('toggle_messaging_platform', { platform, enabled }) },
|
||||
verifyBotToken: (platform, form) => invoke('verify_bot_token', { platform, form }),
|
||||
listConfiguredPlatforms: () => cachedInvoke('list_configured_platforms', {}, 5000),
|
||||
getChannelPluginStatus: (pluginId) => invoke('get_channel_plugin_status', { pluginId }),
|
||||
|
||||
@@ -41,9 +41,13 @@ export async function render() {
|
||||
<div class="config-section-title">快捷链接</div>
|
||||
<div id="links-list"></div>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">关于我们</div>
|
||||
<div id="company-section"></div>
|
||||
</div>
|
||||
<div class="config-section" style="color:var(--text-tertiary);font-size:var(--font-size-xs)">
|
||||
<p>ClawPanel 基于 Tauri v2 构建,前端 Vanilla JS + Vite,后端 Rust。</p>
|
||||
<p style="margin-top:8px">MIT License © 2026 qingchencloud</p>
|
||||
<p style="margin-top:8px">MIT License © 2026 武汉晴辰天下网络科技有限公司</p>
|
||||
</div>
|
||||
`
|
||||
|
||||
@@ -52,6 +56,7 @@ export async function render() {
|
||||
renderProjects(page)
|
||||
renderContribute(page)
|
||||
renderLinks(page)
|
||||
renderCompany(page)
|
||||
return page
|
||||
}
|
||||
|
||||
@@ -470,7 +475,7 @@ const PROJECTS = [
|
||||
},
|
||||
{
|
||||
name: 'OpenClaw-zh',
|
||||
desc: 'AI Agent 框架,支持多模型协作、工具调用、记忆管理-中文优化版',
|
||||
desc: '我们维护的 OpenClaw 汉化版,3000+ Star,中文界面 + 国内镜像优化',
|
||||
url: 'https://github.com/1186258278/OpenClawChineseTranslation',
|
||||
},
|
||||
{
|
||||
@@ -540,3 +545,39 @@ function renderLinks(page) {
|
||||
${LINKS.map(l => `<a class="btn ${l.primary ? 'btn-primary' : 'btn-secondary'} btn-sm" href="${l.url}" target="_blank" rel="noopener">${l.label}</a>`).join('')}
|
||||
</div>`
|
||||
}
|
||||
|
||||
function renderCompany(page) {
|
||||
const el = page.querySelector('#company-section')
|
||||
el.innerHTML = `
|
||||
<div style="display:flex;flex-direction:column;gap:12px">
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<img src="/images/logo-brand.png" alt="晴辰云" style="width:40px;height:40px;border-radius:10px;flex-shrink:0">
|
||||
<div>
|
||||
<div style="font-weight:700;font-size:var(--font-size-md)">武汉晴辰天下网络科技有限公司</div>
|
||||
<div style="font-size:var(--font-size-sm);color:var(--text-secondary)">QingchenCloud</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;font-size:var(--font-size-sm)">
|
||||
<div style="padding:12px;border-radius:var(--radius-md);border:1px solid var(--border-primary);background:var(--bg-secondary)">
|
||||
<div style="color:var(--text-tertiary);font-size:var(--font-size-xs);margin-bottom:4px">官方网站</div>
|
||||
<a href="https://qingchencloud.com" target="_blank" rel="noopener" style="color:var(--accent)">qingchencloud.com</a>
|
||||
</div>
|
||||
<div style="padding:12px;border-radius:var(--radius-md);border:1px solid var(--border-primary);background:var(--bg-secondary)">
|
||||
<div style="color:var(--text-tertiary);font-size:var(--font-size-xs);margin-bottom:4px">产品官网</div>
|
||||
<a href="https://claw.qt.cool" target="_blank" rel="noopener" style="color:var(--accent)">claw.qt.cool</a>
|
||||
</div>
|
||||
<div style="padding:12px;border-radius:var(--radius-md);border:1px solid var(--border-primary);background:var(--bg-secondary)">
|
||||
<div style="color:var(--text-tertiary);font-size:var(--font-size-xs);margin-bottom:4px">开源仓库</div>
|
||||
<a href="https://github.com/qingchencloud" target="_blank" rel="noopener" style="color:var(--accent)">github.com/qingchencloud</a>
|
||||
</div>
|
||||
<div style="padding:12px;border-radius:var(--radius-md);border:1px solid var(--border-primary);background:var(--bg-secondary)">
|
||||
<div style="color:var(--text-tertiary);font-size:var(--font-size-xs);margin-bottom:4px">商务合作</div>
|
||||
<span style="color:var(--text-primary)">请通过官网联系我们</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-tertiary);line-height:1.6">
|
||||
我们是 OpenClaw 汉化版(3000+ Star)和 ClawPanel 的作者团队。日常做 AI Agent 相关的产品和开源工具,也接企业私有化部署、定制开发之类的活儿。有事直接群里找我们就行。
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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'
|
||||
import { QTCOOL, PROVIDER_PRESETS, API_TYPES as SHARED_API_TYPES, fetchQtcoolModels } from '../lib/model-presets.js'
|
||||
|
||||
// ── 常量 ──
|
||||
const STORAGE_KEY = 'clawpanel-assistant'
|
||||
@@ -16,15 +17,6 @@ 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: [] // 始终从 API 动态获取最新模型列表
|
||||
}
|
||||
|
||||
// ── 图片文件存储(通过 Tauri 后端持久化到 ~/.openclaw/clawpanel/images/)──
|
||||
async function saveImageToFile(id, dataUrl) {
|
||||
try { await api.saveImage(id, dataUrl) } catch (e) { console.warn('图片保存失败:', e) }
|
||||
@@ -53,12 +45,8 @@ const MODES = {
|
||||
}
|
||||
const DEFAULT_MODE = 'execute'
|
||||
|
||||
// ── API 类型 ──
|
||||
const API_TYPES = [
|
||||
{ value: 'openai-completions', label: 'OpenAI 兼容 (最常用)' },
|
||||
{ value: 'anthropic-messages', label: 'Anthropic 原生' },
|
||||
{ value: 'google-gemini', label: 'Google Gemini' },
|
||||
]
|
||||
// ── API 类型(从共享模块导入)──
|
||||
const API_TYPES = SHARED_API_TYPES
|
||||
|
||||
function normalizeApiType(raw) {
|
||||
const type = (raw || '').trim()
|
||||
@@ -1445,11 +1433,8 @@ function cleanBaseUrl(raw, apiType) {
|
||||
// Gemini: https://generativelanguage.googleapis.com/v1beta
|
||||
return base
|
||||
}
|
||||
if (/:(11434)$/i.test(base)) return `${base}/v1`
|
||||
if (!base.endsWith('/v1')) {
|
||||
if (/\/v1\/.+/.test(base)) base = base.replace(/\/v1\/.*$/, '/v1')
|
||||
else base += '/v1'
|
||||
}
|
||||
if (/:(11434)$/i.test(base) && !base.endsWith('/v1')) return `${base}/v1`
|
||||
// 不再强制追加 /v1,尊重用户填写的 URL(火山引擎等第三方用 /v3 等路径)
|
||||
return base
|
||||
}
|
||||
|
||||
@@ -2534,6 +2519,12 @@ function showSettings() {
|
||||
<div class="modal-body">
|
||||
<div class="ast-settings-form">
|
||||
<div class="ast-tab-panel active" data-panel="api">
|
||||
<div class="form-group" style="margin-bottom:8px">
|
||||
<label class="form-label">快捷选择</label>
|
||||
<div id="ast-provider-presets" style="display:flex;flex-wrap:wrap;gap:6px">
|
||||
${PROVIDER_PRESETS.map(p => `<button class="btn btn-sm btn-secondary ast-preset-btn" data-key="${p.key}" data-url="${escHtml(p.baseUrl)}" data-api="${p.api}" style="font-size:12px;padding:3px 10px">${p.label}</button>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<div class="form-group" style="flex:1">
|
||||
<label class="form-label">API Base URL</label>
|
||||
@@ -2732,11 +2723,23 @@ function showSettings() {
|
||||
})
|
||||
})
|
||||
|
||||
// API 类型切换时更新提示文本和 placeholder
|
||||
// 服务商快捷预设按钮
|
||||
const apiTypeSelect = overlay.querySelector('#ast-apitype')
|
||||
const apiHintEl = overlay.querySelector('#ast-api-hint')
|
||||
const baseUrlInput = overlay.querySelector('#ast-baseurl')
|
||||
const apiKeyInput = overlay.querySelector('#ast-apikey')
|
||||
overlay.querySelectorAll('.ast-preset-btn').forEach(btn => {
|
||||
btn.onclick = () => {
|
||||
baseUrlInput.value = btn.dataset.url
|
||||
apiTypeSelect.value = btn.dataset.api
|
||||
apiTypeSelect.dispatchEvent(new Event('change'))
|
||||
// 高亮选中
|
||||
overlay.querySelectorAll('.ast-preset-btn').forEach(b => b.style.opacity = '0.5')
|
||||
btn.style.opacity = '1'
|
||||
}
|
||||
})
|
||||
|
||||
// API 类型切换时更新提示文本和 placeholder
|
||||
apiTypeSelect.addEventListener('change', () => {
|
||||
const v = normalizeApiType(apiTypeSelect.value)
|
||||
apiHintEl.textContent = apiHintText(v)
|
||||
@@ -2924,21 +2927,9 @@ function showSettings() {
|
||||
const qtcoolKeyInput = overlay.querySelector('#ast-qtcool-key')
|
||||
const qtcoolUsageLink = overlay.querySelector('#ast-qtcool-usage')
|
||||
|
||||
// 动态获取模型列表
|
||||
// 动态获取模型列表(共享逻辑)
|
||||
;(async () => {
|
||||
let models = QTCOOL.models // fallback
|
||||
try {
|
||||
const resp = await fetch(QTCOOL.baseUrl + '/models', {
|
||||
headers: { 'Authorization': 'Bearer ' + QTCOOL.defaultKey },
|
||||
signal: AbortSignal.timeout(8000)
|
||||
})
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
if (data.data && data.data.length) {
|
||||
models = data.data.map(m => ({ id: m.id, name: m.id })).sort((a, b) => b.id.localeCompare(a.id))
|
||||
}
|
||||
}
|
||||
} catch { /* use fallback */ }
|
||||
const models = await fetchQtcoolModels()
|
||||
qtcoolModelSelect.innerHTML = models.map((m, i) =>
|
||||
`<option value="${m.id}" style="color:#333"${i === 0 ? ' selected' : ''}>${m.name || m.id}${i === 0 ? ' ★' : ''}</option>`
|
||||
).join('')
|
||||
|
||||
@@ -48,18 +48,23 @@ const PLATFORM_REGISTRY = {
|
||||
iconName: 'message-square',
|
||||
desc: '飞书/Lark 企业消息集成,支持文档、多维表格、日历等飞书生态能力',
|
||||
guide: [
|
||||
'<b>选择插件版本</b>:<br>• <b>内置插件</b>(默认)— OpenClaw 自带,主要做聊天入口,安装简单<br>• <b>飞书官方插件</b> — 飞书团队开发,能以你的身份操作飞书(写文档、建表、约日程)<br><span style="color:var(--text-tertiary)">两者互斥,只能启用一个</span>',
|
||||
'前往 <a href="https://open.feishu.cn/app" target="_blank" style="color:var(--accent);text-decoration:underline">飞书开放平台</a>,创建企业自建应用,在「应用能力」中添加<b>机器人</b>能力',
|
||||
'在<b>凭证与基础信息</b>页面获取 <b>App ID</b> 和 <b>App Secret</b>',
|
||||
'进入<b>权限管理</b>,参照 <a href="https://open.larkoffice.com/document/server-docs/application-scope/scope-list" target="_blank" style="color:var(--accent);text-decoration:underline">权限列表</a> 开通所需权限(<code>im:message</code> 等)',
|
||||
'进入<b>事件订阅</b>,选择<b>使用长连接(WebSocket)</b>模式,订阅<b>接收消息</b>和<b>卡片回调</b>事件。如有 user access token 开关请打开',
|
||||
'将 App ID 和 App Secret 填入下方表单,校验后保存。ClawPanel 会自动安装飞书插件并写入配置',
|
||||
'保存后在飞书中向机器人发消息,获取配对码;你可以直接在下方“配对审批”区域粘贴配对码完成绑定,也可以在终端执行 <code>openclaw pairing approve feishu <配对码> --notify</code>',
|
||||
'将 App ID 和 App Secret 填入下方表单,校验后保存',
|
||||
'保存后在飞书中向机器人发消息,获取配对码;你可以直接在下方"配对审批"区域粘贴配对码完成绑定,也可以在终端执行 <code>openclaw pairing approve feishu <配对码> --notify</code>',
|
||||
],
|
||||
guideFooter: '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">国际版 Lark 用户请将域名切换为 <b>lark</b>。详细教程:<a href="https://www.feishu.cn/content/article/7613711414611463386" target="_blank" style="color:var(--accent);text-decoration:underline">OpenClaw 飞书官方插件使用指南</a></div>',
|
||||
guideFooter: '<div style="margin-top:8px;font-size:var(--font-size-xs);color:var(--text-tertiary)">国际版 Lark 用户请将域名切换为 <b>lark</b>。详细教程:<a href="https://www.feishu.cn/content/article/7613711414611463386" target="_blank" style="color:var(--accent);text-decoration:underline">OpenClaw 飞书官方插件使用指南</a> · <a href="https://github.com/AlexAnys/openclaw-feishu" target="_blank" style="color:var(--accent);text-decoration:underline">两个插件怎么选</a></div>',
|
||||
fields: [
|
||||
{ key: 'appId', label: 'App ID', placeholder: 'cli_xxxxxxxxxx', required: true },
|
||||
{ key: 'appSecret', label: 'App Secret', placeholder: '应用密钥', secret: true, required: true },
|
||||
{ key: 'domain', label: '域名', placeholder: 'feishu(国际版选 lark)', required: false },
|
||||
{ key: 'pluginVersion', label: '插件版本', type: 'select', required: false, options: [
|
||||
{ value: 'builtin', label: '内置插件(默认,聊天入口)' },
|
||||
{ value: 'official', label: '飞书官方插件(操作文档/日历/任务)' },
|
||||
]},
|
||||
],
|
||||
pluginRequired: '@openclaw/feishu@latest',
|
||||
pluginId: 'feishu',
|
||||
@@ -293,8 +298,27 @@ async function openConfigDialog(pid, page, state) {
|
||||
</div>
|
||||
`
|
||||
|
||||
// 飞书插件版本检测:根据已安装的插件自动选择
|
||||
if (pid === 'feishu' && !existing.pluginVersion) {
|
||||
try {
|
||||
const officialStatus = await api.getChannelPluginStatus('feishu-openclaw-plugin')
|
||||
if (officialStatus?.installed) existing.pluginVersion = 'official'
|
||||
else existing.pluginVersion = localStorage.getItem('clawpanel-feishu-plugin-version') || 'builtin'
|
||||
} catch { existing.pluginVersion = 'builtin' }
|
||||
}
|
||||
|
||||
const fieldsHtml = reg.fields.map((f, i) => {
|
||||
const val = existing[f.key] || ''
|
||||
if (f.type === 'select' && f.options) {
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${f.label}${f.required ? ' *' : ''}</label>
|
||||
<select class="form-input" name="${f.key}" data-name="${f.key}">
|
||||
${f.options.map(o => `<option value="${o.value}" ${val === o.value ? 'selected' : ''}>${o.label}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return `
|
||||
<div class="form-group">
|
||||
<label class="form-label">${f.label}${f.required ? ' *' : ''}</label>
|
||||
@@ -379,7 +403,7 @@ async function openConfigDialog(pid, page, state) {
|
||||
const collectForm = () => {
|
||||
const obj = {}
|
||||
reg.fields.forEach(f => {
|
||||
const el = modal.querySelector(`input[name="${f.key}"]`)
|
||||
const el = modal.querySelector(`input[name="${f.key}"]`) || modal.querySelector(`select[name="${f.key}"]`)
|
||||
if (el) obj[f.key] = el.value.trim()
|
||||
})
|
||||
return obj
|
||||
@@ -495,7 +519,18 @@ async function openConfigDialog(pid, page, state) {
|
||||
try {
|
||||
// 如果需要安装插件,先安装并显示日志
|
||||
if (reg.pluginRequired) {
|
||||
const pluginId = reg.pluginId || pid
|
||||
// 飞书特殊处理:根据用户选择的插件版本决定安装包
|
||||
let pluginPackage = reg.pluginRequired
|
||||
let pluginId = reg.pluginId || pid
|
||||
if (pid === 'feishu') {
|
||||
const pluginVersionField = modal.querySelector('[data-name="pluginVersion"]')
|
||||
const pluginVersion = pluginVersionField?.value || 'builtin'
|
||||
localStorage.setItem('clawpanel-feishu-plugin-version', pluginVersion)
|
||||
if (pluginVersion === 'official') {
|
||||
pluginPackage = '@larksuiteoapi/feishu-openclaw-plugin'
|
||||
pluginId = 'feishu-openclaw-plugin'
|
||||
}
|
||||
}
|
||||
const pluginStatus = await api.getChannelPluginStatus(pluginId)
|
||||
// 跳过安装:插件已安装 或 已内置(新版 OpenClaw 内置了 feishu 等插件)
|
||||
if (!pluginStatus?.installed && !pluginStatus?.builtin) {
|
||||
@@ -534,7 +569,7 @@ async function openConfigDialog(pid, page, state) {
|
||||
if (pid === 'qqbot') {
|
||||
await api.installQqbotPlugin()
|
||||
} else {
|
||||
await api.installChannelPlugin(reg.pluginRequired, pluginId)
|
||||
await api.installChannelPlugin(pluginPackage, pluginId)
|
||||
}
|
||||
} catch (e) {
|
||||
toast('插件安装失败: ' + e, 'error')
|
||||
|
||||
@@ -49,6 +49,7 @@ let _currentAiBubble = null, _currentAiText = '', _currentAiImages = [], _curren
|
||||
let _isStreaming = false, _isSending = false, _messageQueue = [], _streamStartTime = 0
|
||||
let _lastRenderTime = 0, _renderPending = false, _lastHistoryHash = ''
|
||||
let _streamSafetyTimer = null, _unsubEvent = null, _unsubReady = null, _unsubStatus = null
|
||||
let _seenRunIds = new Set()
|
||||
let _pageActive = false
|
||||
let _errorTimer = null, _lastErrorMsg = null
|
||||
let _attachments = []
|
||||
@@ -886,6 +887,17 @@ function handleChatEvent(payload) {
|
||||
if (payload.sessionKey && payload.sessionKey !== _sessionKey && _sessionKey) return
|
||||
|
||||
const { state } = payload
|
||||
const runId = payload.runId
|
||||
|
||||
// 重复 run 过滤:跳过已完成的 runId 的后续事件(Gateway 可能对同一消息触发多个 run)
|
||||
if (runId && state === 'final' && _seenRunIds.has(runId)) {
|
||||
console.log('[chat] 跳过重复 final, runId:', runId)
|
||||
return
|
||||
}
|
||||
if (runId && state === 'delta' && _seenRunIds.has(runId) && !_isStreaming) {
|
||||
console.log('[chat] 跳过已完成 run 的 delta, runId:', runId)
|
||||
return
|
||||
}
|
||||
|
||||
if (state === 'delta') {
|
||||
const c = extractChatContent(payload.message)
|
||||
@@ -935,6 +947,14 @@ function handleChatEvent(payload) {
|
||||
const hasContent = finalText || _currentAiImages.length || _currentAiVideos.length || _currentAiAudios.length || _currentAiFiles.length
|
||||
// 忽略空 final(Gateway 会为一条消息触发多个 run,部分是空 final)
|
||||
if (!_currentAiBubble && !hasContent) return
|
||||
// 标记 runId 为已处理,防止重复
|
||||
if (runId) {
|
||||
_seenRunIds.add(runId)
|
||||
if (_seenRunIds.size > 200) {
|
||||
const first = _seenRunIds.values().next().value
|
||||
_seenRunIds.delete(first)
|
||||
}
|
||||
}
|
||||
showTyping(false)
|
||||
// 如果流式阶段没有创建 bubble,从 final message 中提取
|
||||
if (!_currentAiBubble && hasContent) {
|
||||
|
||||
@@ -247,7 +247,7 @@ function renderList(page, state) {
|
||||
const btn = e.currentTarget
|
||||
btn.disabled = true
|
||||
try {
|
||||
await wsClient.request('cron.run', { name: jid })
|
||||
await wsClient.request('cron.run', { id: jid })
|
||||
toast('任务已触发执行', 'success')
|
||||
setTimeout(() => fetchJobs(page, state), 2000)
|
||||
} catch (err) { toast('触发失败: ' + err, 'error') }
|
||||
@@ -259,7 +259,7 @@ function renderList(page, state) {
|
||||
btn.disabled = true
|
||||
btn.innerHTML = icon('refresh-cw', 14)
|
||||
try {
|
||||
await wsClient.request('cron.update', { name: jid, patch: { enabled: !job.enabled } })
|
||||
await wsClient.request('cron.update', { id: jid, patch: { enabled: !job.enabled } })
|
||||
toast(job.enabled ? '已暂停' : '已启用', 'info')
|
||||
await fetchJobs(page, state)
|
||||
} catch (err) { toast('操作失败: ' + err, 'error'); btn.disabled = false; btn.innerHTML = job.enabled ? icon('pause', 14) : icon('play', 14) }
|
||||
@@ -273,7 +273,7 @@ function renderList(page, state) {
|
||||
if (!yes) return
|
||||
if (btn) btn.disabled = true
|
||||
try {
|
||||
await wsClient.request('cron.remove', { name: jid })
|
||||
await wsClient.request('cron.remove', { id: jid })
|
||||
toast('已删除', 'info')
|
||||
await fetchJobs(page, state)
|
||||
} catch (err) { toast('删除失败: ' + err, 'error'); if (btn) btn.disabled = false }
|
||||
@@ -404,7 +404,7 @@ async function openTaskDialog(job, page, state) {
|
||||
patch.schedule = { kind: 'cron', expr: schedule }
|
||||
patch.payload = { kind: 'agentTurn', message }
|
||||
if (agentId) patch.agentId = agentId
|
||||
await wsClient.request('cron.update', { name: job.id, patch })
|
||||
await wsClient.request('cron.update', { id: job.id, patch })
|
||||
toast('任务已更新', 'success')
|
||||
} else {
|
||||
const params = {
|
||||
|
||||
@@ -301,7 +301,13 @@ function bindActions(page) {
|
||||
try {
|
||||
const config = await api.readOpenclawConfig()
|
||||
const port = config?.gateway?.port || 18789
|
||||
const url = `http://127.0.0.1:${port}`
|
||||
// 远程部署时使用当前浏览器域名/IP,桌面版用 127.0.0.1
|
||||
const host = window.__TAURI_INTERNALS__ ? '127.0.0.1' : (location.hostname || '127.0.0.1')
|
||||
const proto = location.protocol === 'https:' ? 'https' : 'http'
|
||||
let url = `${proto}://${host}:${port}`
|
||||
// 如果 Gateway 配置了 token 鉴权,附加到 URL 方便直接访问
|
||||
const authToken = config?.gateway?.auth?.token
|
||||
if (authToken) url += `?token=${encodeURIComponent(authToken)}`
|
||||
// 尝试多种方式打开浏览器
|
||||
if (window.__TAURI_INTERNALS__) {
|
||||
try {
|
||||
|
||||
@@ -190,7 +190,11 @@ async function loadClusterOverview(page) {
|
||||
const detail = page.querySelector('#infra-detail')
|
||||
if (detail) detail.textContent = `${nodes.length} 节点 · ${runningContainers} 运行 / ${totalContainers} 总计`
|
||||
} catch (e) {
|
||||
page.querySelector('#cluster-stats').innerHTML = `<span class="cluster-stat" style="color:var(--error,#ef4444)">${icon('x-circle', 12)} Docker 未连接: ${esc(e.message)}</span>`
|
||||
const errMsg = String(e.message || e)
|
||||
// 后端未运行(Tauri 桌面版不含 Docker 后端,或 Web 模式后端未启动)
|
||||
const isBackendMissing = errMsg.includes('后端服务未运行') || errMsg.includes('is not valid JSON') || errMsg.includes('<!DOCTYPE')
|
||||
const displayMsg = isBackendMissing ? 'Docker 管理后端未运行' : errMsg
|
||||
page.querySelector('#cluster-stats').innerHTML = `<span class="cluster-stat" style="color:var(--error,#ef4444)">${icon('x-circle', 12)} Docker 未连接: ${esc(displayMsg)}</span>`
|
||||
|
||||
// ClawPanel 自身运行在 Docker 容器中时,显示容器内专属指引
|
||||
if (isInDocker()) {
|
||||
@@ -218,6 +222,31 @@ async function loadClusterOverview(page) {
|
||||
return
|
||||
}
|
||||
|
||||
// 后端缺失时显示专属指引(桌面版需要 Web 部署模式)
|
||||
if (isBackendMissing) {
|
||||
page.querySelector('#workers-grid').innerHTML = `
|
||||
<div class="docker-empty">
|
||||
<div class="docker-empty-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="48" height="48"><rect x="1" y="11" width="4" height="3" rx=".5"/><rect x="6" y="11" width="4" height="3" rx=".5"/><rect x="11" y="11" width="4" height="3" rx=".5"/><rect x="6" y="7" width="4" height="3" rx=".5"/><rect x="11" y="7" width="4" height="3" rx=".5"/><rect x="16" y="11" width="4" height="3" rx=".5"/><rect x="11" y="3" width="4" height="3" rx=".5"/><path d="M2 17c1 3 4 5 10 5s9-2 10-5"/></svg>
|
||||
</div>
|
||||
<div class="docker-empty-title">龙虾军团需要 Web 部署模式</div>
|
||||
<div class="docker-empty-desc">Docker 容器管理功能需要 ClawPanel Web 后端支持。桌面版暂不内置 Docker 管理后端。</div>
|
||||
<div class="docker-guide-section">
|
||||
<div class="docker-guide-title">${icon('info', 14)} 如何使用龙虾军团</div>
|
||||
<ol>
|
||||
<li>使用 Docker 部署 ClawPanel Web 版(推荐):<code style="font-size:11px">docker run -d -p 3000:3000 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/qingchencloud/openclaw:latest</code></li>
|
||||
<li>或使用开发模式启动:<code style="font-size:11px">npm run dev</code>,后端会自动启动 Docker 管理服务</li>
|
||||
<li>确保 Docker Desktop 已安装并运行</li>
|
||||
</ol>
|
||||
<div style="margin-top:8px;font-size:12px;color:var(--text-tertiary)">桌面版的 Docker 管理功能正在开发中,敬请期待后续版本更新。</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
page.querySelector('#docker-nodes').innerHTML = ''
|
||||
page.querySelector('#docker-containers').innerHTML = ''
|
||||
return
|
||||
}
|
||||
|
||||
const isWin = navigator.userAgent.includes('Windows')
|
||||
const isMacOS = navigator.userAgent.includes('Mac')
|
||||
const installGuide = isWin
|
||||
|
||||
@@ -6,60 +6,7 @@ import { api } from '../lib/tauri-api.js'
|
||||
import { toast } from '../components/toast.js'
|
||||
import { showModal, showConfirm } from '../components/modal.js'
|
||||
import { icon, statusIcon } from '../lib/icons.js'
|
||||
|
||||
// API 接口类型选项
|
||||
const API_TYPES = [
|
||||
{ value: 'openai-completions', label: 'OpenAI 兼容 (最常用)' },
|
||||
{ value: 'anthropic-messages', label: 'Anthropic 原生' },
|
||||
{ value: 'openai-responses', label: 'OpenAI Responses' },
|
||||
{ value: 'google-gemini', label: 'Google Gemini' },
|
||||
]
|
||||
|
||||
// 服务商快捷预设
|
||||
const PROVIDER_PRESETS = [
|
||||
{ key: 'openai', label: 'OpenAI 官方', baseUrl: 'https://api.openai.com/v1', api: 'openai-completions' },
|
||||
{ key: 'anthropic', label: 'Anthropic 官方', baseUrl: 'https://api.anthropic.com', api: 'anthropic-messages' },
|
||||
{ key: 'deepseek', label: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', api: 'openai-completions' },
|
||||
{ key: 'google', label: 'Google Gemini', baseUrl: 'https://generativelanguage.googleapis.com/v1beta', api: 'google-gemini' },
|
||||
{ key: 'ollama', label: 'Ollama (本地)', baseUrl: 'http://127.0.0.1:11434/v1', api: 'openai-completions' },
|
||||
]
|
||||
|
||||
// gpt.qt.cool 推广配置
|
||||
const QTCOOL = {
|
||||
baseUrl: 'https://gpt.qt.cool/v1',
|
||||
defaultKey: 'sk-0JDu7hyc51ZKD4iNebpFu07EUEhXmVVc',
|
||||
site: 'https://gpt.qt.cool/',
|
||||
usageUrl: 'https://gpt.qt.cool/user?key=',
|
||||
providerKey: 'qtcool',
|
||||
api: 'openai-completions',
|
||||
models: [] // 不使用硬编码模型列表,始终从 API 动态获取最新列表
|
||||
}
|
||||
|
||||
// 常用模型预设(按服务商分组)
|
||||
const MODEL_PRESETS = {
|
||||
openai: [
|
||||
{ id: 'gpt-4o', name: 'GPT-4o', contextWindow: 128000 },
|
||||
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', contextWindow: 128000 },
|
||||
{ id: 'o3-mini', name: 'o3 Mini', contextWindow: 200000, reasoning: true },
|
||||
],
|
||||
anthropic: [
|
||||
{ id: 'claude-sonnet-4-5-20250514', name: 'Claude Sonnet 4.5', contextWindow: 200000 },
|
||||
{ id: 'claude-haiku-3-5-20241022', name: 'Claude Haiku 3.5', contextWindow: 200000 },
|
||||
],
|
||||
deepseek: [
|
||||
{ id: 'deepseek-chat', name: 'DeepSeek V3', contextWindow: 64000 },
|
||||
{ id: 'deepseek-reasoner', name: 'DeepSeek R1', contextWindow: 64000, reasoning: true },
|
||||
],
|
||||
google: [
|
||||
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', contextWindow: 1000000, reasoning: true },
|
||||
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash', contextWindow: 1000000 },
|
||||
],
|
||||
ollama: [
|
||||
{ id: 'qwen2.5:7b', name: 'Qwen 2.5 7B', contextWindow: 32768 },
|
||||
{ id: 'llama3.2', name: 'Llama 3.2', contextWindow: 8192 },
|
||||
{ id: 'gemma3', name: 'Gemma 3', contextWindow: 32768 },
|
||||
],
|
||||
}
|
||||
import { API_TYPES, PROVIDER_PRESETS, QTCOOL, MODEL_PRESETS, fetchQtcoolModels } from '../lib/model-presets.js'
|
||||
|
||||
export async function render() {
|
||||
const page = document.createElement('div')
|
||||
@@ -431,13 +378,8 @@ function normalizeProviderUrls(config) {
|
||||
if (!url.endsWith('/v1')) url += '/v1'
|
||||
} else if (apiType !== 'google-gemini') {
|
||||
// Ollama 端口检测:11434 默认需要加 /v1
|
||||
if (/:11434$/.test(url)) url += '/v1'
|
||||
// 其他 OpenAI 兼容: 确保有 /v1
|
||||
if (!url.endsWith('/v1')) {
|
||||
const idx = url.indexOf('/v1/')
|
||||
if (idx >= 0) url = url.slice(0, idx + 3)
|
||||
else url += '/v1'
|
||||
}
|
||||
if (/:11434$/.test(url) && !url.endsWith('/v1')) url += '/v1'
|
||||
// 不再强制追加 /v1,尊重用户填写的 URL(火山引擎等第三方用 /v3 等路径)
|
||||
}
|
||||
p.baseUrl = url
|
||||
}
|
||||
@@ -786,23 +728,8 @@ function bindTopActions(page, state) {
|
||||
btn.textContent = '获取模型列表...'
|
||||
btn.disabled = true
|
||||
|
||||
// 动态获取模型列表,失败则用静态 fallback
|
||||
let models = QTCOOL.models
|
||||
try {
|
||||
const resp = await fetch(QTCOOL.baseUrl + '/models', {
|
||||
headers: { 'Authorization': 'Bearer ' + QTCOOL.defaultKey },
|
||||
signal: AbortSignal.timeout(8000)
|
||||
})
|
||||
if (resp.ok) {
|
||||
const data = await resp.json()
|
||||
if (data.data && data.data.length) {
|
||||
models = data.data.map(m => ({
|
||||
id: m.id, name: m.id, contextWindow: 128000,
|
||||
reasoning: m.id.includes('codex')
|
||||
})).sort((a, b) => b.id.localeCompare(a.id))
|
||||
}
|
||||
}
|
||||
} catch { /* use fallback */ }
|
||||
// 动态获取模型列表(共享逻辑)
|
||||
const models = await fetchQtcoolModels()
|
||||
|
||||
btn.innerHTML = `${icon('zap', 14)} 一键添加全部模型`
|
||||
btn.disabled = false
|
||||
|
||||