Skip to content

Commit 48cffe1

Browse files
committed
feat: v0.9.2 — SkillHub双源技能管理、消息渠道多Agent绑定、模型配置优化、白屏安全网等
1 parent b55ba4c commit 48cffe1

25 files changed

+1080
-268
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ nul
2727
release_body.md
2828
release_body.md.bak
2929

30+
# 本地临时文件 / 设计文档
31+
.tmp/
32+
3033
# 内部开发文档(不入公开仓)
3134
BLOCKING_ISSUES_REPORT.md
3235
__clawapp-chat-ref.js

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@
55
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
66
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)
77

8+
## [0.9.2] - 2026-03-16
9+
10+
### 新功能 (Features)
11+
12+
- **SkillHub + ClawHub 双源技能管理** — Skills 页面新增已安装/搜索安装 Tab 切换,支持 SkillHub 和 ClawHub 双源下拉选择、搜索安装、卸载功能
13+
- **SkillHub CLI 集成** — 新增 SkillHub 检测、安装、搜索、安装 Skill 的完整后端命令链(Rust + Web 双模式)
14+
- **消息渠道多 Agent 绑定展示** — 已接入列表现在显示所有绑定的 Agent 标签,不再只显示第一个
15+
- **消息渠道快速绑定 Agent** — 已接入平台点击"绑定新 Agent"弹出简化的 Agent 选择弹窗,无需重新填写凭证
16+
- **消息渠道多账号支持(飞书)** — 后端 save_messaging_platform 支持 accountId 参数,可将不同飞书应用绑定到不同 Agent
17+
- **NVM_SYMLINK 环境变量支持** — Windows 下 nvm 用户的 Node.js 路径检测更可靠
18+
19+
### 修复 (Fixes)
20+
21+
- **Skills JSON 解析修复** — extractCliJson 函数正确处理 CLI 输出中混入的 Node.js 警告信息
22+
- **`--verbose` 日志污染** — 移除 openclaw skills 命令中多余的 --verbose 参数,避免输出被 npm 日志污染
23+
- **SkillHub 搜索结果解析** — 修复实际 CLI 输出格式与预期不符导致的搜索结果为空
24+
- **Windows cmd /c 兼容** — SkillHub/npx/ClawHub 命令在 Windows 上正确通过 cmd /c 调用
25+
- **Cron delivery 参数格式** — 定时任务投递参数修复为正确的 mode+to+channel 格式
26+
- **白屏安全网** — boot() 增加 try-catch 和 splash 超时检测,WebView2 加载失败时不再白屏
27+
28+
### 改进 (Improvements)
29+
30+
- **Git HTTPS 重写规则扩展** — 从 6 条扩展到 14 条,覆盖 GitHub/GitLab/Bitbucket 的所有 SSH/Git 协议变体
31+
- **Agent 管理直接读 openclaw.json** — 不再通过 CLI 获取 Agent 列表,响应速度大幅提升
32+
- **记忆文件直接读 openclaw.json** — Agent workspace 路径从配置文件直接解析,避免 CLI 调用阻塞
33+
- **NSIS 中文语言选择器** — Windows 安装包默认中文,支持语言选择
34+
- **WebView2 内嵌引导安装** — NSIS 安装包内嵌 WebView2 bootstrapper,离线环境也能安装
35+
- **模型添加体验优化** — 模型页面快捷添加改为模型选择弹窗,用户可自主勾选需要的模型
36+
- **助手系统提示词精简** — 移除冗余信息,聚焦技术支持核心能力
37+
838
## [0.8.6] - 2026-03-13
939

1040
### 修复 (Fixes)

docs/update/latest.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"version": "0.9.1",
3-
"minAppVersion": "0.9.1",
4-
"hash": "sha256:6b762c7ec210b138110c86b240d7b4b1a24b32205ff7e33a249a8d91d310f328",
5-
"url": "https://github.com/qingchencloud/clawpanel/releases/download/v0.9.1/web-0.9.1.zip",
6-
"size": 1984123,
7-
"changelog": "",
8-
"releasedAt": "2026-03-14T12:15:17Z"
2+
"version": "0.9.2",
3+
"minAppVersion": "0.9.0",
4+
"hash": "",
5+
"url": "https://github.com/qingchencloud/clawpanel/releases/download/v0.9.2/web-0.9.2.zip",
6+
"size": 0,
7+
"changelog": "SkillHub 双源技能管理、消息渠道多 Agent 绑定、模型配置优化、白屏安全网等",
8+
"releasedAt": "2026-03-16T03:00:00Z"
99
}

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
<div class="sp-bar"><div class="sp-bar-inner"></div></div>
6767
<div class="sp-site"><a href="https://qt.cool" target="_blank">qt.cool</a></div>
6868
</div>
69-
<script>setTimeout(function(){var s=document.getElementById('splash');if(s){s.classList.add('hide');setTimeout(function(){s.remove()},500)}},6000)</script>
69+
<script>setTimeout(function(){var s=document.getElementById('splash');if(s){var app=document.getElementById('content');if(app&&app.children.length>0){s.classList.add('hide');setTimeout(function(){s.remove()},500)}else{s.innerHTML='<div style="text-align:center;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif"><div style="font-size:40px;margin-bottom:12px">\u26A0\uFE0F</div><div style="font-size:16px;font-weight:600;color:#18181b;margin-bottom:8px">\u9875\u9762\u52A0\u8F7D\u5931\u8D25</div><div style="font-size:12px;color:#71717a;margin-bottom:16px;line-height:1.6">\u53EF\u80FD\u539F\u56E0\uFF1AWebView2 \u7F3A\u5931\u6216\u7248\u672C\u8FC7\u4F4E<br>\u8BF7\u5B89\u88C5\u6700\u65B0\u7248 <a href="https://go.microsoft.com/fwlink/p/?LinkId=2124703" style="color:#6366f1">WebView2 Runtime</a></div><button onclick="location.reload()" style="padding:6px 16px;border-radius:6px;border:none;background:#6366f1;color:#fff;font-size:12px;cursor:pointer">\u5237\u65B0\u91CD\u8BD5</button></div>'}}},8000)</script>
7070

7171
<div id="app">
7272
<aside id="sidebar"></aside>

openclaw-version-policy.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@
2323
"chinese": {
2424
"recommended": "2026.3.13-zh.1"
2525
}
26+
},
27+
"0.9.2": {
28+
"official": {
29+
"recommended": "2026.3.13"
30+
},
31+
"chinese": {
32+
"recommended": "2026.3.13-zh.1"
33+
}
2634
}
2735
}
2836
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "clawpanel",
3-
"version": "0.9.1",
3+
"version": "0.9.2",
44
"private": true,
55
"description": "ClawPanel - OpenClaw 可视化管理面板,基于 Tauri v2 的跨平台桌面应用",
66
"type": "module",

scripts/dev-api.js

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,35 @@ function clearLoginAttempts(ip) {
290290
_loginAttempts.delete(ip)
291291
}
292292

293+
// 从 CLI 输出中提取 JSON(跳过 Node 警告、npm 更新提示等非 JSON 行)
294+
function extractCliJson(text) {
295+
// 快速路径:整个文本就是合法 JSON
296+
try { return JSON.parse(text) } catch {}
297+
// 找到第一个 { 或 [ 开始尝试解析
298+
for (let i = 0; i < text.length; i++) {
299+
const ch = text[i]
300+
if (ch === '{' || ch === '[') {
301+
// 找到匹配的闭合位置
302+
let depth = 0, end = -1
303+
const close = ch === '{' ? '}' : ']'
304+
let inStr = false, esc = false
305+
for (let j = i; j < text.length; j++) {
306+
const c = text[j]
307+
if (esc) { esc = false; continue }
308+
if (c === '\\' && inStr) { esc = true; continue }
309+
if (c === '"' && !esc) { inStr = !inStr; continue }
310+
if (inStr) continue
311+
if (c === ch) depth++
312+
else if (c === close) { depth--; if (depth === 0) { end = j; break } }
313+
}
314+
if (end > i) {
315+
try { return JSON.parse(text.slice(i, end + 1)) } catch {}
316+
}
317+
}
318+
}
319+
throw new Error('解析失败: 输出中未找到有效 JSON')
320+
}
321+
293322
// 配置缓存:避免每次请求同步读磁盘(TTL 2秒,写入时立即失效)
294323
let _panelConfigCache = null
295324
let _panelConfigCacheTime = 0
@@ -1334,7 +1363,7 @@ const handlers = {
13341363
return { exists: true, values: form }
13351364
},
13361365

1337-
save_messaging_platform({ platform, form }) {
1366+
save_messaging_platform({ platform, form, accountId }) {
13381367
if (!fs.existsSync(CONFIG_PATH)) throw new Error('openclaw.json 不存在')
13391368
const cfg = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'))
13401369
if (!cfg.channels) cfg.channels = {}
@@ -1356,6 +1385,14 @@ const handlers = {
13561385
entry.appSecret = form.appSecret
13571386
entry.connectionMode = 'websocket'
13581387
if (form.domain) entry.domain = form.domain
1388+
// 多账号模式:写入 channels.feishu.accounts.<accountId>
1389+
if (accountId) {
1390+
if (!cfg.channels.feishu) cfg.channels.feishu = { enabled: true }
1391+
if (!cfg.channels.feishu.accounts) cfg.channels.feishu.accounts = {}
1392+
cfg.channels.feishu.accounts[accountId] = entry
1393+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2))
1394+
return { ok: true }
1395+
}
13591396
} else {
13601397
Object.assign(entry, form)
13611398
}
@@ -3006,8 +3043,8 @@ const handlers = {
30063043
skills_list() {
30073044
// 尝试真实 CLI
30083045
try {
3009-
const out = execSync('npx -y openclaw skills list --json --verbose', { encoding: 'utf8', timeout: 30000 })
3010-
return JSON.parse(out)
3046+
const out = execSync('npx -y openclaw skills list --json', { encoding: 'utf8', timeout: 30000 })
3047+
return extractCliJson(out)
30113048
} catch {
30123049
// CLI 不可用时返回 mock 数据
30133050
return {
@@ -3026,15 +3063,15 @@ const handlers = {
30263063
skills_info({ name }) {
30273064
try {
30283065
const out = execSync(`npx -y openclaw skills info ${JSON.stringify(name)} --json`, { encoding: 'utf8', timeout: 30000 })
3029-
return JSON.parse(out)
3066+
return extractCliJson(out)
30303067
} catch (e) {
30313068
throw new Error('查看详情失败: ' + (e.message || e))
30323069
}
30333070
},
30343071
skills_check() {
30353072
try {
30363073
const out = execSync('npx -y openclaw skills check --json', { encoding: 'utf8', timeout: 30000 })
3037-
return JSON.parse(out)
3074+
return extractCliJson(out)
30383075
} catch {
30393076
return { summary: { total: 0, eligible: 0, disabled: 0, blocked: 0, missingRequirements: 0 }, eligible: [], disabled: [], blocked: [], missingRequirements: [] }
30403077
}
@@ -3055,6 +3092,76 @@ const handlers = {
30553092
throw new Error(`安装失败: ${e.message || e}`)
30563093
}
30573094
},
3095+
skills_skillhub_check() {
3096+
try {
3097+
const out = execSync('skillhub --version', { encoding: 'utf8', timeout: 5000 })
3098+
return { installed: true, version: out.trim() }
3099+
} catch {
3100+
return { installed: false }
3101+
}
3102+
},
3103+
skills_skillhub_setup({ cliOnly }) {
3104+
const flag = cliOnly ? '--cli-only' : '--no-skills'
3105+
try {
3106+
const out = execSync(
3107+
`curl -fsSL https://skillhub-1388575217.cos.ap-guangzhou.myqcloud.com/install/install.sh | bash -s -- ${flag}`,
3108+
{ encoding: 'utf8', timeout: 120000 }
3109+
)
3110+
return { success: true, output: out.trim() }
3111+
} catch (e) {
3112+
throw new Error('SkillHub 安装失败: ' + (e.message || e))
3113+
}
3114+
},
3115+
skills_skillhub_search({ query }) {
3116+
const q = String(query || '').trim()
3117+
if (!q) return []
3118+
try {
3119+
const out = execSync(`skillhub search ${JSON.stringify(q)}`, { encoding: 'utf8', timeout: 30000 })
3120+
// 解析格式: [N] owner/repo/name 状态\n 统计 描述...
3121+
const lines = out.split('\n')
3122+
const items = []
3123+
for (let i = 0; i < lines.length; i++) {
3124+
const trimmed = lines[i].trim()
3125+
if (!trimmed.startsWith('[')) continue
3126+
const bracketEnd = trimmed.indexOf(']')
3127+
if (bracketEnd < 0) continue
3128+
const afterBracket = trimmed.slice(bracketEnd + 1).trim()
3129+
const slug = (afterBracket.split(/\s/)[0] || '').trim()
3130+
if (!slug.includes('/')) continue
3131+
let desc = ''
3132+
if (i + 1 < lines.length) {
3133+
const next = lines[i + 1].trim()
3134+
const starIdx = next.indexOf('⭐')
3135+
if (starIdx >= 0) {
3136+
const afterStar = next.slice(starIdx + 2).trim()
3137+
desc = afterStar.replace(/^[\d.]+[kKmM]?\s*/, '').trim()
3138+
}
3139+
}
3140+
items.push({ slug, description: desc, source: 'skillhub' })
3141+
}
3142+
return items
3143+
} catch (e) {
3144+
throw new Error('搜索失败: ' + (e.message || e) + '。请先安装 SkillHub CLI')
3145+
}
3146+
},
3147+
skills_skillhub_install({ slug }) {
3148+
const skillsDir = path.join(OPENCLAW_DIR, 'skills')
3149+
if (!fs.existsSync(skillsDir)) fs.mkdirSync(skillsDir, { recursive: true })
3150+
try {
3151+
const out = execSync(`skillhub install ${JSON.stringify(slug)} --force`, { cwd: homedir(), encoding: 'utf8', timeout: 120000 })
3152+
return { success: true, slug, output: out.trim() }
3153+
} catch (e) {
3154+
throw new Error('安装失败: ' + (e.message || e) + '。请先安装 SkillHub CLI')
3155+
}
3156+
},
3157+
3158+
skills_uninstall({ name }) {
3159+
if (!name || name.includes('..') || name.includes('/') || name.includes('\\')) throw new Error('无效的 Skill 名称')
3160+
const skillDir = path.join(OPENCLAW_DIR, 'skills', name)
3161+
if (!fs.existsSync(skillDir)) throw new Error(`Skill「${name}」不存在`)
3162+
fs.rmSync(skillDir, { recursive: true, force: true })
3163+
return { success: true, name }
3164+
},
30583165
skills_clawhub_search({ query }) {
30593166
const q = String(query || '').trim()
30603167
if (!q) return []

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "clawpanel"
3-
version = "0.9.1"
3+
version = "0.9.2"
44
edition = "2021"
55
description = "ClawPanel - OpenClaw 可视化管理面板"
66
authors = ["qingchencloud"]

src-tauri/src/commands/agent.rs

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,86 @@ use serde_json::Value;
44
use std::fs;
55
use std::io::Write;
66

7-
/// 获取 agent 列表
7+
/// 获取 agent 列表(直接读 openclaw.json,不走 CLI,毫秒级响应)
88
#[tauri::command]
99
pub async fn list_agents() -> Result<Value, String> {
10-
let output = openclaw_command_async()
11-
.args(["agents", "list", "--json"])
12-
.output()
13-
.await
14-
.map_err(|e| {
15-
if e.kind() == std::io::ErrorKind::NotFound {
16-
"OpenClaw CLI 未找到,请确认已安装并重启 ClawPanel。\n如果使用 nvm 安装,请从终端启动 ClawPanel。".to_string()
17-
} else {
18-
format!("执行失败: {e}")
19-
}
20-
})?;
21-
22-
if !output.status.success() {
23-
let stderr = String::from_utf8_lossy(&output.stderr);
24-
return Err(format!("获取 Agent 列表失败: {stderr}"));
10+
let config_path = super::openclaw_dir().join("openclaw.json");
11+
if !config_path.exists() {
12+
return Err("openclaw.json 不存在,请先安装 OpenClaw".to_string());
2513
}
14+
let content = fs::read_to_string(&config_path).map_err(|e| format!("读取配置失败: {e}"))?;
15+
let config: Value =
16+
serde_json::from_str(&content).map_err(|e| format!("解析 JSON 失败: {e}"))?;
2617

27-
let stdout = String::from_utf8_lossy(&output.stdout);
28-
crate::commands::skills::extract_json_pub(&stdout)
29-
.ok_or_else(|| "解析 JSON 失败: 输出中未找到有效 JSON".to_string())
18+
let agents_list = config
19+
.get("agents")
20+
.and_then(|a| a.get("list"))
21+
.and_then(|l| l.as_array())
22+
.cloned()
23+
.unwrap_or_default();
24+
25+
// 补全 main agent 的 workspace(config 中可能没有显式指定)
26+
let default_workspace = config
27+
.get("agents")
28+
.and_then(|a| a.get("defaults"))
29+
.and_then(|d| d.get("workspace"))
30+
.and_then(|w| w.as_str())
31+
.map(|s| s.to_string())
32+
.unwrap_or_else(|| {
33+
super::openclaw_dir()
34+
.join("workspace")
35+
.to_string_lossy()
36+
.to_string()
37+
});
38+
39+
let enriched: Vec<Value> = agents_list
40+
.into_iter()
41+
.map(|mut agent| {
42+
let id = agent
43+
.get("id")
44+
.and_then(|v| v.as_str())
45+
.unwrap_or("")
46+
.to_string();
47+
// 补全 workspace 路径
48+
if agent.get("workspace").and_then(|w| w.as_str()).is_none()
49+
|| agent.get("workspace").and_then(|w| w.as_str()) == Some("")
50+
{
51+
if id == "main" {
52+
agent.as_object_mut().map(|o| {
53+
o.insert(
54+
"workspace".to_string(),
55+
Value::String(default_workspace.clone()),
56+
)
57+
});
58+
} else {
59+
let ws = super::openclaw_dir()
60+
.join("agents")
61+
.join(&id)
62+
.join("workspace")
63+
.to_string_lossy()
64+
.to_string();
65+
agent
66+
.as_object_mut()
67+
.map(|o| o.insert("workspace".to_string(), Value::String(ws)));
68+
}
69+
}
70+
// 补全 identityName 用于前端显示
71+
let identity_name = agent
72+
.get("identity")
73+
.and_then(|i| i.get("name"))
74+
.and_then(|n| n.as_str())
75+
.unwrap_or("")
76+
.to_string();
77+
if !identity_name.is_empty() {
78+
agent
79+
.as_object_mut()
80+
.map(|o| o.insert("identityName".to_string(), Value::String(identity_name)));
81+
}
82+
agent
83+
})
84+
.collect();
85+
86+
Ok(Value::Array(enriched))
3087
}
3188

3289
/// 创建新 agent

0 commit comments

Comments
 (0)