Skip to content

Commit 0af5b28

Browse files
committed
release: prepare v1.20.9 remote management fixes
1 parent 8705bb2 commit 0af5b28

21 files changed

Lines changed: 1199 additions & 329 deletions

CHANGELOG-zh.md

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

8+
## [1.20.9] - 2026-05-23
9+
10+
### 发布概览
11+
- **远程管理稳定性与 QwenPaw 支持** — 将“远程工作区”统一更名为“远程管理”,修复远程 Skills 发现链路,使已检测到的 Agent 能稳定展示真实 Skills,并将 QwenPaw 作为内置 Lobster 智能体接入。
12+
13+
### 用户可见更新
14+
- **远程 Skill 发现修复** — 远程管理现在会在重新进入主机时加载已缓存的远程 Skill 快照,扫描时校验标准 `SKILL.md` 标记,并正确识别 Lobster 类嵌套 Skill 目录,不再只显示空的 Agent 外壳。
15+
- **远程管理交互对齐** — 统一新远程模块命名,补齐主机与详情页的数据回填行为,并保持与全局工作区一致的概览/详情交互模型。
16+
- **QwenPaw 内置智能体** — 新增 QwenPaw 内置 Lobster 智能体,默认 Skill 根目录为 `~/.qwenpaw/skill_pool`,并补充本地图标资源。
17+
18+
### 开发者与治理更新
19+
- **远程扫描 API 补全** — 新增 `remote_list_skill_snapshots` 命令,使前端可以直接读取已持久化的远程扫描结果,而不必每次强制重新走 SSH 扫描。
20+
- **扫描一致性回归覆盖** — 扩展远程工作区测试,覆盖 `SKILL.md` 校验、额外扫描根目录和 Lobster 递归目录发现,确保远程扫描规则与本地扫描器保持一致。
821
## [1.20.8] - 2026-05-23
922

1023
### 发布概览

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.20.9] - 2026-05-23
9+
10+
### Release Overview
11+
- **Remote Management Stabilization and QwenPaw Support** — Renamed Remote Workspace to Remote Management, fixed remote Skill discovery so detected agents surface their real Skills reliably, and added built-in QwenPaw support under the Lobster agent group.
12+
13+
### User-facing
14+
- **Remote Skill Discovery Fixes** — Remote Management now reloads cached remote Skill snapshots when you re-open a host, verifies standard `SKILL.md` markers during scans, and correctly discovers nested Lobster-style Skill directories instead of showing empty agent shells.
15+
- **Remote Management UX Alignment** — Updated the new remote section naming, refined host/detail loading behavior, and kept the page model aligned with the Global Workspace overview/detail flow.
16+
- **QwenPaw Built-in Agent** — Added QwenPaw as a built-in Lobster agent with the default Skill root `~/.qwenpaw/skill_pool` and a bundled local icon asset.
17+
18+
### Developer & Governance
19+
- **Remote Scan API Completion** — Added a dedicated `remote_list_skill_snapshots` command so the frontend can reload persisted remote scan results without forcing a fresh SSH scan.
20+
- **Scanner Parity Coverage** — Extended remote workspace tests to cover `SKILL.md` validation, additional scan roots, and recursive Lobster directory discovery so remote scanning now follows the same compatibility rules as local scanning.
821
## [1.20.8] - 2026-05-23
922

1023
### Release Overview

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "skills-manager-plus",
33
"private": true,
4-
"version": "1.20.8",
4+
"version": "1.20.9",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

public/agent-icons/qwenpaw.png

54.4 KB
Loading

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 = "skills-manager-plus"
3-
version = "1.20.8"
3+
version = "1.20.9"
44
description = "Skills-Manager-Plus - AI Agent Skills Central Manager"
55
authors = ["tianliang"]
66
license = "MIT"

src-tauri/src/commands/remote_workspace.rs

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ use crate::core::{
1515
remote_workspace::{
1616
create_remote_host, delete_remote_host, delete_remote_skill, get_remote_host,
1717
insert_remote_operation_log, install_center_skill_to_remote, list_remote_agent_paths,
18-
list_remote_hosts, list_remote_operation_logs, parse_ssh_config, scan_remote_host,
19-
update_remote_host, update_remote_skill_from_center, RemoteAgentPathRecord, RemoteAuthType,
20-
RemoteHostInput, RemoteHostRecord, RemoteOperationLogRecord, RemoteSkillSnapshot,
21-
SshConfigHost,
18+
list_remote_hosts, list_remote_operation_logs, list_remote_skill_snapshots,
19+
parse_ssh_config, scan_remote_host, update_remote_host,
20+
update_remote_skill_from_center, CredentialRefUpdate, RemoteAgentPathRecord,
21+
RemoteAuthType, RemoteHostInput, RemoteHostRecord, RemoteOperationLogRecord,
22+
RemoteSkillSnapshot, SshConfigHost,
2223
},
2324
skill_store::SkillStore,
2425
};
@@ -43,18 +44,25 @@ pub async fn remote_create_host(
4344
let store = store.inner().clone();
4445
tauri::async_runtime::spawn_blocking(move || {
4546
let host = create_remote_host(&store, input.clone(), None).map_err(AppError::db)?;
46-
let Some(saved_ref) = save_host_secret(
47+
let credential_ref_update = save_host_secret(
4748
&host.id,
4849
&input.auth_type,
50+
&input.auth_type,
4951
password,
5052
passphrase,
5153
save_secret,
52-
)?
53-
else {
54-
return Ok(host);
55-
};
54+
)?;
5655

57-
update_remote_host(&store, &host.id, input, Some(saved_ref)).map_err(AppError::db)
56+
match credential_ref_update {
57+
CredentialRefUpdate::Replace(Some(saved_ref)) => update_remote_host(
58+
&store,
59+
&host.id,
60+
input,
61+
CredentialRefUpdate::Replace(Some(saved_ref)),
62+
)
63+
.map_err(AppError::db),
64+
CredentialRefUpdate::Replace(None) | CredentialRefUpdate::Preserve => Ok(host),
65+
}
5866
})
5967
.await?
6068
}
@@ -70,9 +78,18 @@ pub async fn remote_update_host(
7078
) -> Result<RemoteHostRecord, AppError> {
7179
let store = store.inner().clone();
7280
tauri::async_runtime::spawn_blocking(move || {
73-
let credential_ref =
74-
save_host_secret(&id, &input.auth_type, password, passphrase, save_secret)?;
75-
update_remote_host(&store, &id, input, credential_ref).map_err(AppError::db)
81+
let existing_host = get_remote_host(&store, &id)
82+
.map_err(AppError::db)?
83+
.ok_or_else(|| AppError::not_found("Remote host not found"))?;
84+
let credential_ref_update = save_host_secret(
85+
&id,
86+
&existing_host.auth_type,
87+
&input.auth_type,
88+
password,
89+
passphrase,
90+
save_secret,
91+
)?;
92+
update_remote_host(&store, &id, input, credential_ref_update).map_err(AppError::db)
7693
})
7794
.await?
7895
}
@@ -311,6 +328,18 @@ pub async fn remote_list_agent_paths(
311328
.await?
312329
}
313330

331+
#[tauri::command]
332+
pub async fn remote_list_skill_snapshots(
333+
store: State<'_, Arc<SkillStore>>,
334+
host_id: String,
335+
) -> Result<Vec<RemoteSkillSnapshot>, AppError> {
336+
let store = store.inner().clone();
337+
tauri::async_runtime::spawn_blocking(move || {
338+
list_remote_skill_snapshots(&store, &host_id).map_err(AppError::db)
339+
})
340+
.await?
341+
}
342+
314343
#[tauri::command]
315344
pub async fn remote_list_operation_logs(
316345
store: State<'_, Arc<SkillStore>>,
@@ -327,31 +356,56 @@ pub async fn remote_list_operation_logs(
327356

328357
fn save_host_secret(
329358
host_id: &str,
359+
previous_auth_type: &RemoteAuthType,
330360
auth_type: &RemoteAuthType,
331361
password: Option<String>,
332362
passphrase: Option<String>,
333363
save_secret_requested: bool,
334-
) -> Result<Option<String>, AppError> {
364+
) -> Result<CredentialRefUpdate, AppError> {
365+
let auth_type_changed = previous_auth_type != auth_type;
366+
367+
if auth_type_changed {
368+
delete_secret(&credential_ref(host_id, RemoteSecretKind::Password))
369+
.map_err(AppError::internal)?;
370+
delete_secret(&credential_ref(
371+
host_id,
372+
RemoteSecretKind::PrivateKeyPassphrase,
373+
))
374+
.map_err(AppError::internal)?;
375+
}
376+
335377
if !save_secret_requested {
336-
return Ok(None);
378+
return Ok(if auth_type_changed {
379+
CredentialRefUpdate::Replace(None)
380+
} else {
381+
CredentialRefUpdate::Preserve
382+
});
337383
}
338384

339385
match auth_type {
340386
RemoteAuthType::Password => {
341387
let Some(password) = non_empty_secret(password) else {
342-
return Ok(None);
388+
return Ok(if auth_type_changed {
389+
CredentialRefUpdate::Replace(None)
390+
} else {
391+
CredentialRefUpdate::Preserve
392+
});
343393
};
344394
let reference = credential_ref(host_id, RemoteSecretKind::Password);
345395
save_secret(&reference, &password).map_err(AppError::internal)?;
346-
Ok(Some(reference))
396+
Ok(CredentialRefUpdate::Replace(Some(reference)))
347397
}
348398
RemoteAuthType::PrivateKey => {
349399
let Some(passphrase) = non_empty_secret(passphrase) else {
350-
return Ok(None);
400+
return Ok(if auth_type_changed {
401+
CredentialRefUpdate::Replace(None)
402+
} else {
403+
CredentialRefUpdate::Preserve
404+
});
351405
};
352406
let reference = credential_ref(host_id, RemoteSecretKind::PrivateKeyPassphrase);
353407
save_secret(&reference, &passphrase).map_err(AppError::internal)?;
354-
Ok(Some(reference))
408+
Ok(CredentialRefUpdate::Replace(Some(reference)))
355409
}
356410
}
357411
}

src-tauri/src/core/remote_agent_detector.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,34 @@ use crate::core::tool_adapters::default_tool_adapters;
66
pub struct RemoteAgentCandidate {
77
pub agent_id: String,
88
pub display_name: String,
9+
pub detect_path: String,
910
pub skills_path: String,
11+
pub additional_scan_paths: Vec<String>,
1012
pub recursive_scan: bool,
1113
}
1214

1315
pub fn remote_agent_candidates(home_dir: &str) -> Vec<RemoteAgentCandidate> {
1416
default_tool_adapters()
1517
.into_iter()
16-
.map(|adapter| RemoteAgentCandidate {
17-
agent_id: adapter.key,
18-
display_name: adapter.display_name,
19-
skills_path: join_remote_home(home_dir, &adapter.relative_skills_dir),
20-
recursive_scan: adapter.recursive_scan,
18+
.map(|adapter| {
19+
let mut additional_scan_paths = Vec::new();
20+
for path in adapter.additional_scan_dirs {
21+
let normalized = join_remote_home(home_dir, &path);
22+
if normalized != join_remote_home(home_dir, &adapter.relative_skills_dir)
23+
&& !additional_scan_paths.contains(&normalized)
24+
{
25+
additional_scan_paths.push(normalized);
26+
}
27+
}
28+
29+
RemoteAgentCandidate {
30+
agent_id: adapter.key,
31+
display_name: adapter.display_name,
32+
detect_path: join_remote_home(home_dir, &adapter.relative_detect_dir),
33+
skills_path: join_remote_home(home_dir, &adapter.relative_skills_dir),
34+
additional_scan_paths,
35+
recursive_scan: adapter.recursive_scan,
36+
}
2137
})
2238
.collect()
2339
}
@@ -71,10 +87,16 @@ mod tests {
7187
let candidates = remote_agent_candidates("/home/dev/");
7288

7389
assert!(candidates.iter().any(|candidate| {
74-
candidate.agent_id == "codex" && candidate.skills_path == "/home/dev/.agents/skills"
90+
candidate.agent_id == "codex"
91+
&& candidate.detect_path == "/home/dev/.codex"
92+
&& candidate.skills_path == "/home/dev/.agents/skills"
93+
&& candidate
94+
.additional_scan_paths
95+
.contains(&"/home/dev/.codex/skills".to_string())
7596
}));
7697
assert!(candidates.iter().any(|candidate| {
7798
candidate.agent_id == "claude_code"
99+
&& candidate.detect_path == "/home/dev/.claude"
78100
&& candidate.skills_path == "/home/dev/.claude/skills"
79101
}));
80102
}

0 commit comments

Comments
 (0)