From 9beaca58efe8f3e3989f813501ef2c3e1986df8a Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Wed, 4 Nov 2026 14:57:49 +0800 Subject: [PATCH] fix(dsh): harden favorites interactions and prepare 0.6.2 Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../add-dsh-native-plugin/favorites-design.md | 8 ++ .../add-dsh-native-plugin/release-guide.md | 18 +++-- .../add-dsh-native-plugin/release-notes.md | 23 +++++- .../changes/add-dsh-native-plugin/tasks.md | 4 +- plugins/dsh/README.en.md | 18 +++-- plugins/dsh/README.md | 18 +++-- plugins/dsh/package.json | 2 +- plugins/dsh/src/client/catalog-page.tsx | 74 ++++++++++++++++--- .../dsh/src/client/native-control-styles.tsx | 2 + plugins/dsh/src/client/skill-card.tsx | 10 +-- plugins/dsh/src/client/use-favorites.ts | 11 ++- plugins/dsh/test/favorites-client.test.ts | 61 ++++++++++++++- plugins/dsh/test/market-card.test.tsx | 9 ++- 13 files changed, 217 insertions(+), 41 deletions(-) diff --git a/openspec/changes/add-dsh-native-plugin/favorites-design.md b/openspec/changes/add-dsh-native-plugin/favorites-design.md index 856707d2b..a88cc4865 100644 --- a/openspec/changes/add-dsh-native-plugin/favorites-design.md +++ b/openspec/changes/add-dsh-native-plugin/favorites-design.md @@ -6,6 +6,12 @@ 收藏卡不展示快照版本为最新。用户点击该项详情或安装时,仅解析这一项当前可下载版本,再走既有精确版本详情和安装确认;不批量后台解析收藏、不静默装旧快照。离线或下架时保留收藏,仍可取消。详情的官网链接保留。 +详情的使用或安装主操作放在右侧安装管理标题区,复用已有紧凑按钮;摘要下不重复放置主操作。详情指标明确标注“市场收藏”,与本机书签区分,不能将本机收藏状态或操作计入市场统计。 + +取消收藏成功后提供可关闭的“已取消收藏”通知,显示对应技能名称并允许撤销最近一次取消。撤销以明确的收藏true重用既有写入,不切换布尔状态、不增加接口;失败保留重试入口。提示不定时消失,关闭、后续取消、离开页面或切换来源时结束;不能在新来源重放旧撤销。手动重新收藏同项清除过期提示。卡片或通知按钮消失时,仅在焦点仍属于该操作且未转移的情况下恢复至可用入口,不抢占新交互焦点。 + +提交新的收藏搜索或清空条件会取消尚未完成的详情/安装解析,迟到结果不得覆盖新的搜索意图。无匹配结果提供清空搜索,区别于当前来源真正没有收藏。解析失败后取消该项收藏,应同步清除针对该项的过期重试提示。 + ## 存储与边界 存储在 canonical DSH_HOME 下 `skillhub/favorites.json`,独立于inventory v2与设置草稿;相同DSH_HOME的Profile/项目共享,不提供收藏安装范围选择,不同DSH_HOME隔离。身份为规范化Registry来源+namespace+slug,名称只是快照,版本不进入身份。切源只显示新来源收藏,保留旧来源记录。 @@ -26,6 +32,8 @@ Query缓存按sourceRevision隔离;来源切换后的旧查询、保存或解析结果不能进入新来源UI。成功持久化后再呈现保存成功;多次交互/多控件写入需避免返回旧全量列表覆盖新状态。保存失败保留原状态并提供重试,不能阻断市场浏览。收藏筛选期间不请求市场分页;切回后保留市场查询/排序/标签语义。 +写入成功先取消对应来源的活动读取,再使用Host返回的完整列表更新共享缓存,防止保存期间启动的旧读取迟到覆盖结果,不立即追加listFavorites;写入失败仍重新读取以核对“已提交但响应丢失”的情况。进入页面、窗口重新聚焦和重连时允许刷新以同步其他Host修改,不定时轮询。收藏筛选和搜索仅处理内存列表,不逐项请求详情或版本。 + ## 验收 正常:市场收藏→详情同态→完整收藏列表→重启读取→取消,不改变安装、库存或服务端starCount。变更:跨分页与本地搜索、两来源同名隔离、迟到响应/取消、最新版本解析、下架/离线保留、跨进程RMW、未知/损坏/超限/symlink不覆盖。以独立磁盘字节、HTTP记录、子进程屏障和真实DOM作为Oracle,不只mock返回值。 diff --git a/openspec/changes/add-dsh-native-plugin/release-guide.md b/openspec/changes/add-dsh-native-plugin/release-guide.md index d1a3cbc07..b5ea1a967 100644 --- a/openspec/changes/add-dsh-native-plugin/release-guide.md +++ b/openspec/changes/add-dsh-native-plugin/release-guide.md @@ -10,7 +10,15 @@ 插件运行在 DSH 主机,不部署进 SkillHub 网站,也不要求网站嵌入插件 UI。插件内部 Remote 接口随插件版本交付;Registry API 随服务端交付。两者分别管理版本和发布节奏。 -### 1.1 当前候选与发布顺序 +### 1.1 当前候选:DSH 0.6.2 补丁版 + +当前公开 npm 版本为 DSH `0.6.1`,本轮候选为 `@astron-team/skillhub-dsh@0.6.2`,独立标签 `dsh-v0.6.2`。范围包含自 `dsh-v0.6.1` 以来的多来源隔离、升级变化与本地文件处理、单技能 ZIP 导入导出、RC5 适配和本机收藏,详见 [发布说明](release-notes.md)。仅走 DSH 专用 npm 工作流,不创建 SkillHub 服务端 Release、不重复发布 CLI、不部署 SaaS。 + +合并最终修正后冻结精确 main 提交;通过候选检查、支持 RC 的隔离 Host 与浏览器验收,再以该提交创建新标签。专用工作流检查和发布的 tarball 必须逐字节关联;发布后比较公开 `dist.integrity`,从 npm 下载实际包完成干净安装,并核对升级保留已有来源、技能、库存 v2 和收藏。既有功能证据仅在输入等价时复用,最终包和公开包验收单独记录;不得修改旧标签或覆盖已发布版本。共享库存格式不变,不要求用户同步升级 CLI。 + +本轮仅修改本机插件和交付文档,不涉及服务端/网页镜像、数据库或公网路由,因此不运行 SkillHub 服务 Docker/Hong Kong 部署门禁;由实际插件 tarball、官方 DSH Host 和公开 npm 安装替代对应产物与运行验证,不将其称为 SaaS 全链路验收。 + +### 1.2 历史:0.6.1 发布顺序 CLI `0.3.0`(`cli-v0.3.0`)已发布,本轮继续发布 DSH `0.6.1`(`dsh-v0.6.1`),范围为技能启停、统一只读检查更新、紧凑已安装卡片,以及发布前库存兼容修复。不发布服务端镜像或部署 SaaS。 @@ -25,7 +33,7 @@ CLI `0.3.0`(`cli-v0.3.0`)已发布,本轮继续发布 DSH `0.6.1`(`dsh-v 插件内置核心,不调用 CLI。回退不能只恢复库存:须核对入口、内容、元数据和日志,不能用过时备份覆盖新记录。完整协议以 [启停设计 §7](skill-toggle-design.md#7-库存协议与实施门禁) 为准。 -### 1.2 历史:0.4.0 库存升级说明 +### 1.3 历史:0.4.0 库存升级说明 此前库存升级交付 CLI `0.2.0` 和 DSH 插件 `0.4.0`,不发布服务端镜像、不部署 SaaS。插件内置安装核心,不要求另装 CLI;但同一用户使用 CLI 共同管理技能时,两者必须使用兼容库存格式。 @@ -105,7 +113,7 @@ Host 准备允许通过显式 `--download-cache` 共享受校验临时目录中 ### 2.3 隔离环境验证 -本次 `0.6.1` 候选分别验证 DSH `0.2.0-rc.2` 与 `0.2.1-rc.2`,Node `>=24.2.0 <25`。Host、app-boot、核心模块及注入 Client 模块须为同一精确发行版。新 RC 仅使用 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`;旧 RC 的 Cordis、Loader、Include、Timer 须为已验证的完整组合:`4.0.4 / 1.0.5 / 1.0.9 / 1.1.6` 或 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`,不得交叉混装;以实际 Host 解析结果为准。已发布 `0.1.0` 尚不包含后一组合支持及历史回执修复,`0.1.1` 起包含这些修复,`0.2.0` 延续此兼容基线;发布前使用本次 tarball 验证。在独立测试账户或容器中准备 Web Profile、工作区和测试技能,连接测试 Registry。仅隔离 Profile/DSH home 不足以隔离共享 inventory。 +本次 `0.6.2` 候选分别验证 DSH `0.2.0-rc.2`、`0.2.1-rc.2` 与 `0.2.1-rc.5`,Node `>=24.2.0 <25`。Host、app-boot、核心模块及注入 Client 模块须为同一精确发行版。新 RC 仅使用 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`;旧 RC 的 Cordis、Loader、Include、Timer 须为已验证的完整组合:`4.0.4 / 1.0.5 / 1.0.9 / 1.1.6` 或 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7`,不得交叉混装;以实际 Host 解析结果为准。已发布 `0.1.0` 尚不包含后一组合支持及历史回执修复,`0.1.1` 起包含这些修复,`0.2.0` 延续此兼容基线;发布前使用本次 tarball 验证。在独立测试账户或容器中准备 Web Profile、工作区和测试技能,连接测试 Registry。仅隔离 Profile/DSH home 不足以隔离共享 inventory。 首次创建测试 Web Profile,然后安装本地候选包: @@ -190,7 +198,7 @@ SaaS 使用其现有流水线部署;公共接口契约、测试数据规则与 ### 4.1 独立发布流程 -插件使用独立版本,tag 前缀固定为 `dsh-v`(本次为 `dsh-v0.6.1`),不复用 CLI 的 `cli-v*` 发布流程。专用工作流为 `.github/workflows/release-dsh-plugin.yml`;发布前仍须确认 npm 包名、兼容基线及候选包验收,本次候选为 `0.6.1`,manifest 已显式设为 `private: false`;缺省 Registry 为 `https://skill.xfyun.cn`,显式空串/空白仍断开,已有自定义来源保留。发布前分别验证缺省、断开和自定义配置,以及正式域名的固定版本安装、状态识别和本地管理。正式发布仍以最终提交、候选验收和审批结果为准。 +插件使用独立版本,tag 前缀固定为 `dsh-v`(本次为 `dsh-v0.6.2`),不复用 CLI 的 `cli-v*` 发布流程。专用工作流为 `.github/workflows/release-dsh-plugin.yml`;发布前仍须确认 npm 包名、兼容基线及候选包验收,本次候选为 `0.6.2`,manifest 已显式设为 `private: false`;缺省 Registry 为 `https://skill.xfyun.cn`,显式空串/空白仍断开,已有自定义来源保留。发布前分别验证缺省、断开和自定义配置,以及正式域名的固定版本安装、状态识别和本地管理。正式发布仍以最终提交、候选验收和审批结果为准。 发布流程依次完成:校验版本 → 测试与构建 → 生成并检查 tarball → 隔离 DSH 候选验收 → 发布审批 → 发布 npm → 校验已发布包。当前工作流自动执行版本校验、测试与构建、包检查、上传及完整性校验;隔离 DSH 的 UI、真实模型和平台验收按本次确认范围另行执行,未验项目须单独注明;CI 矩阵成功不等同于这些验收通过。工作流使用仓库的 `NPM_TOKEN` 发布凭证,源码及 npm 包不包含凭证;配置了环境保护规则时还须完成对应审批。 @@ -213,7 +221,7 @@ SaaS 使用其现有流水线部署;公共接口契约、测试数据规则与 ### 4.3 发布说明 -`0.6.1` 的变更说明见 [release-notes.md](release-notes.md)。正式版本发布前按最终候选及验收结果更新,不沿用开发版本的支持声明。 +`0.6.2` 的变更说明见 [release-notes.md](release-notes.md)。正式版本发布前按最终候选及验收结果更新,不沿用开发版本的支持声明。 每个版本包含: diff --git a/openspec/changes/add-dsh-native-plugin/release-notes.md b/openspec/changes/add-dsh-native-plugin/release-notes.md index 98ca6e7a3..ac3d95288 100644 --- a/openspec/changes/add-dsh-native-plugin/release-notes.md +++ b/openspec/changes/add-dsh-native-plugin/release-notes.md @@ -1,7 +1,22 @@ # SkillHub DSH 原生插件发布说明 -## 0.6.1 候选:启停、检查更新与紧凑卡片 +## 0.6.2 补丁候选:来源、变更预览、技能包与收藏 + +本轮仅发布 `@astron-team/skillhub-dsh@0.6.2`,使用独立标签 `dsh-v0.6.2`。不重复发布 CLI,不创建 SkillHub 服务端 Release、不发布服务镜像或部署 SaaS。当前仍为候选;发布工作流、公开字节与干净安装通过前不宣称已发布。 + +- **多来源与更新隔离**:[#1047](https://github.com/iflytek/skillhub/pull/1047) 保留不同来源的安装记录,切换来源后仅检查当前来源技能的更新,不认领其他来源同名技能。[#1052](https://github.com/iflytek/skillhub/pull/1052) 将来源选择与编辑集中于设置卡片,简化日常入口并明确管理引用。 +- **升级前查看变化**:[#1055](https://github.com/iflytek/skillhub/pull/1055) 先核对本地修改,支持确认后还原当前安装版本的文件修改,或备份后继续查看新版本变化;只有再次确认才执行升级。按文件懒加载、有高度限制;不提供技能版本降级或升级后回滚。 +- **文件操作可靠性**:[#1057](https://github.com/iflytek/skillhub/pull/1057) 补齐中断文件清理的恢复保护,并提供上下文失效诊断;不将诊断补充宣称为所有宿主失效原因均已修复。Windows 可用性测试保留真实失败信息。 +- **单技能 ZIP 导入导出**:[#1061](https://github.com/iflytek/skillhub/pull/1061) 本机预检、确认导入与按安装位置导出,保留修改和资源;本地导入不冒充市场来源、不参与市场更新检查,导入拒绝覆盖已有同名目录。 +- **本机收藏**:[#1065](https://github.com/iflytek/skillhub/pull/1065) 卡片和详情书签、当前来源完整收藏筛选及本地搜索;不需要登录、不修改市场收藏数。取消支持撤销最近一次操作,失败可重试;来源切换与迟到响应隔离,搜索不会被旧打开请求打断。 +- **性能与交互修正**:收藏成功采用 Host 返回的完整列表,避免写后重复读取;失败仍补读核对。详情主操作归入右侧安装管理,指标明确区分市场收藏和本机书签,并补键盘焦点与窄屏处理。收藏不后台逐项解析版本,只有打开某项详情或安装时读取其当前版本;缺失或离线时保留收藏。 +- **宿主兼容与依赖维护**:[#1063](https://github.com/iflytek/skillhub/pull/1063) 增加 DSH `0.2.1-rc.5`,保留两个旧 RC2 与完整依赖组合的校验;[#1058](https://github.com/iflytek/skillhub/pull/1058) 更新受影响的锁定依赖。Host Node 范围仍为 `>=24.2.0 <25`。 +- **兼容边界**:共享 inventory 继续保持 v2;收藏与本地导入使用 DSH 私有状态,不要求用户同步升级 CLI。本轮不新增 SaaS Registry API;浏览器、真实 Host、CI、模型与公开包验收分别记录,既有结果不能替代最终包验证。 + +**完整插件变化**:[dsh-v0.6.1...dsh-v0.6.2](https://github.com/iflytek/skillhub/compare/dsh-v0.6.1...dsh-v0.6.2)。 + +## 0.6.1:启停、检查更新与紧凑卡片 - **受管理技能启停**:[#1025](https://github.com/iflytek/skillhub/pull/1025) 按确认的位置将 `SKILL.md` 转换为 `SKILL.md.disabled`,启用恢复入口,保留目录和资源。更新停用安装后仍保持停用;文件修改、共享归属、目标被占用及不可信记录仍阻止写操作。停用不撤回已加载会话内容,也不停止任务。 - **统一检查、逐项更新**:[#1030](https://github.com/iflytek/skillhub/pull/1030) 主动只读检查当前来源库存,渐进显示结果和失败重试。一次可信快照、有界并发、同来源技能共用远端 resolve;取消及上下文变化不接纳迟到结果。真正更新仍进入详情按位置确认,不提供批量自动覆盖。 @@ -9,7 +24,7 @@ - **库存兼容**:共享库存保持严格 v2,不写入 `installationState`;启停状态从可信的 `SKILL.md` / `SKILL.md.disabled` 入口派生,不要求 CLI 用户同步升级。已发布 CLI `0.2.0` 的正常查询和普通目标管理必须继续兼容;默认更新、同步及 Suite 不得无意重新启用停用目标。旧 CLI 显式 force 重装、更新、同步仍按原覆盖语义执行,可能恢复标准入口;显式 remove 仍可删除,新版重新读取须反映真实变化。 [#1033](https://github.com/iflytek/skillhub/pull/1033) 已完成未发布 v3 的安全转换、普通/启停日志兼容隔离及真实旧包交替验收。 - **交付边界**:保留现有双 RC、Node/Cordis 与来源边界;详情官网链接仅使用 API 的安全 `webUrl`,缺失即隐藏。该增量不包含 SaaS 部署、场景助手或技能清单导入导出。 -上述功能及库存兼容修复已合并。CLI `0.3.0` 已发布;DSH `0.6.1` 仍是候选,不把历史功能验收等同最终版本包或公开包验收。发布顺序和限制见 [发布指南](release-guide.md),结果由 [验证记录](verification.md) 单独维护。 +上述功能及库存兼容修复已合并,CLI `0.3.0` 与 DSH `0.6.1` 已公开发布;不把历史功能验收等同后续最终版本包或公开包验收。发布顺序和限制见 [发布指南](release-guide.md),结果由 [验证记录](verification.md) 单独维护。 ## 0.5.0 技能可用性诊断 @@ -20,7 +35,7 @@ ## 版本与交付状态 -当前准备版本为 DSH `0.6.1`;CLI `0.3.0` 已发布并完成公开包验收,不重复发布。保留已存在的 `dsh-v0.6.0` 标签,DSH 候选使用新标签 `dsh-v0.6.1`,尚不据此声明 DSH 已公开发布。历史版本说明保留;实际公开状态以对应 npm 版本和专用发布工作流为准。 +当前准备版本为 DSH `0.6.2`;公开 npm 最新版本为 `0.6.1`。CLI `0.3.0` 已发布,本轮不重复发布。保留既有 `dsh-v0.6.0`、`dsh-v0.6.1` 标签,不移动历史引用;新候选使用 `dsh-v0.6.2`。实际公开状态以对应 npm 版本和专用发布工作流为准。 `@astron-team/skillhub-dsh@0.3.1` 已发布,独立标签为 `dsh-v0.3.1`。[发布准备 PR #995](https://github.com/iflytek/skillhub/pull/995) 已合并,[专用发布工作流](https://github.com/iflytek/skillhub/actions/runs/39556075587) 成功;[公开 npm 包](https://www.npmjs.com/package/@astron-team/skillhub-dsh/v/0.3.1) 与工作流产物逐字节一致,发布时 `latest` 指向 `0.3.1`。两版 Linux 的严格安装、原生探针及浏览器操作复验通过,测试工件与公开包字节一致,结果见 [验证摘要](verification.md)。历史已发布版本保持不变。 @@ -110,7 +125,7 @@ Node 保持 `>=24.2.0 <25`。精确兼容与验证边界见 [兼容设计](compa ## 安装、更新与维护 -`0.6.1` 公开可用后,可通过 `dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.1 --ignore-scripts` 安装到已有 Web Profile;发布前使用本地候选 tarball。发布前从源码执行 `bun run check` 后生成 `.tgz`,使用固定版本 DSH 的 `plugin ... add` 安装。具体准备、命令、配置和测试流程见 [中文指南](../../../plugins/dsh/README.md)、[英文指南](../../../plugins/dsh/README.en.md) 及 [发布指南](release-guide.md)。 +`0.6.2` 公开可用后,可通过 `dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.2 --ignore-scripts` 安装到已有 Web Profile;发布前使用本地候选 tarball。发布前从源码执行 `bun run check` 后生成 `.tgz`,使用固定版本 DSH 的 `plugin ... add` 安装。具体准备、命令、配置和测试流程见 [中文指南](../../../plugins/dsh/README.md)、[英文指南](../../../plugins/dsh/README.en.md) 及 [发布指南](release-guide.md)。 更新或回退前保留原包和校验值、停止新操作并处理待恢复事务,然后停止 Host、安装兼容包并重新启动。浏览器旧资源可强制刷新;这不能代替 Host 依赖重载。禁用或移除包不会卸载技能,不应删除技能目录、inventory 或操作日志。 diff --git a/openspec/changes/add-dsh-native-plugin/tasks.md b/openspec/changes/add-dsh-native-plugin/tasks.md index 7de242c06..8ecfe71a3 100644 --- a/openspec/changes/add-dsh-native-plugin/tasks.md +++ b/openspec/changes/add-dsh-native-plugin/tasks.md @@ -483,6 +483,8 @@ Chromium 145.0.7632.6连接真实RC5 Host与本地Registry夹具,验证目录/ - [x] 54.2 独立收藏store与Host RPC:来源隔离、完整读取、明确状态幂等写、官方锁/原子写、数据校验与稳定错误;不改inventory或既有安装门禁。 - [x] 54.3 复用卡片/详情书签和浏览页收藏筛选,完整本地搜索、最新版本打开、同步/迟到/失败状态与键盘焦点。 - [x] 54.4 以独立磁盘/HTTP/跨进程Oracle验证正常、边界、错误、并发和来源切换,执行构建、类型、lint、测试及包/生成契约检查。 -- [ ] 54.5 三RC真实Host偏好/RPC回归、RC5浏览器/重启/分页/主题/390px验收、独立评审及人工预览;发布后置。 +- [x] 54.5 三RC真实Host偏好/RPC回归、RC5浏览器/重启/分页/主题/390px验收、独立评审及人工预览;发布后置。 第53章后续CI已在PR #1063完成:30项通过、7项条件跳过,包含19个DSH组合;用户人工确认后已合并,临时预览与模型进程已关闭。它不替代第54章新增功能验收。 + +第54章验证摘要(2026-11-04):构建、类型、lint及761测试/18037断言通过;三个已支持RC的Linux真实Host完成收藏RPC、来源隔离及跨Profile重启验证。Chromium 145连接真实RC5 Host,覆盖分页外收藏、完整本地搜索、当前版本解析、来源切换竞争、损坏数据保护及明暗主题/390px。补充8项边界交互:取消与撤销、失败重试、重复撤销仅一次写入、手动重新收藏、慢解析期间重新搜索、空结果清除、切源淘汰旧撤销及详情返回焦点;页面异常为0。成功保存直接采用完整返回列表,取消活动读取以阻止迟到覆盖,失败仍补读核验。用户确认预览并授权合并。此前PR提交的跨平台及19个DSH组合CI通过;最终合并提交和0.6.2发布候选的CI、包完整性及公开安装仍单独验收,不能由历史结果替代。多窗口在重新聚焦/重入时刷新,不承诺实时广播;极限500卡DOM性能和真实模型执行未验证,不新增SaaS接口、不修改inventory v2。 diff --git a/plugins/dsh/README.en.md b/plugins/dsh/README.en.md index cbac3c6d1..5941a3f6a 100644 --- a/plugins/dsh/README.en.md +++ b/plugins/dsh/README.en.md @@ -14,6 +14,8 @@ Version `0.4.0` uses the independent `dsh-v0.4.0` tag. It adds versioned example `0.6.1` includes managed enable/disable, unified read-only update checks, and compact horizontal installed cards with a More menu. Shared inventory remains v2 without a coordinated CLI upgrade. Compatibility acceptance with the actual CLI 0.2.0 package has passed. Check npm for public availability. +`0.6.2` is the current patch candidate, combining source-isolated updates, release-change previews and local-file handling, single-skill ZIP transfer, local favorites, and DSH `0.2.1-rc.5` compatibility. Shared inventory remains v2 without a coordinated CLI upgrade. Candidate and public-package acceptance are separate; check npm before installation. + ## Use an installed skill Market, installed and everyday conversation lists show one card per skill. “使用技能” asks the Host to verify the unique installation actually usable in the current session. Catalog use creates a conversation in the same workspace; conversation use keeps the current session. Choose an example or custom task to insert an editable draft. This never sends a message, invokes a model or overwrites the original draft. @@ -23,7 +25,7 @@ In the installed card’s More menu, “管理安装” opens skill details, whe An active session is required. Shadowed skills, modified files, disabled user invocation or changed context are reported without modifying the draft. Historical receipts are not proof of current installation. This feature is available from `0.4.0`. -## Local package transfer (in development, not published) +## Local package transfer (0.6.2) Use **Import skill package** in the installed view to select a ZIP containing a root `SKILL.md`, preview it, choose the current project or all projects, and confirm. Existing same-name directories are never overwritten. The first version accepts one skill per archive, up to 8 MiB compressed; existing expanded-size, file-count and per-file limits still apply. @@ -35,7 +37,7 @@ In the detail's installation management panel, each location's Manage menu expor | Component | Requirement or scope | | --- | --- | -| DSH | source candidate: `0.2.0-rc.2`, `0.2.1-rc.2`, or `0.2.1-rc.5`; Host, app-boot and injected Client modules must share the exact release | +| DSH | `0.6.2` candidate: `0.2.0-rc.2`, `0.2.1-rc.2`, or `0.2.1-rc.5`; Host, app-boot and injected Client modules must share the exact release | | Cordis | Old RC: both families below; `0.2.1-rc.2` and `0.2.1-rc.5`: only `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7` | | Host Node | `>=24.2.0 <25`; Node 24.0.0 is unsuitable | | Build package manager | Bun `1.3.13`, using frozen lockfiles | @@ -47,7 +49,7 @@ Windows browser acceptance does not establish complete native Windows Host files Version `0.3.1` includes the merged [Issue #981 compatibility fix](https://github.com/iflytek/skillhub/pull/988). Published plugin `0.3.0` does not include it. Unknown releases and mixed Host/Client graphs reject new writes. Optional `userQuestions` is enabled by Host service availability; it is not part of the mandatory write-version gate. Its API is identical in the three enumerated RCs, each of which the peer declaration permits. An absent service falls back to the plugin confirmation dialog. -The source candidate adds `0.2.1-rc.5`; compatibility and acceptance are tracked in the [compatibility design](../../openspec/changes/add-dsh-native-plugin/compatibility-design.md) and task 53 verification. This does not extend the published npm `0.6.1` support declaration. Platform, Node boundary, browser, and model evidence remain separate; release is a separate step. +The `0.6.2` candidate adds `0.2.1-rc.5`; compatibility and acceptance are tracked in the [compatibility design](../../openspec/changes/add-dsh-native-plugin/compatibility-design.md) and task 53 verification. This does not extend the published npm `0.6.1` support declaration. Platform, Node boundary, browser, and model evidence remain separate; release is a separate step. ## Build and package @@ -88,8 +90,8 @@ Published `0.1.0` still requires the original Cordis `4.0.4` baseline and does n Query the exact npm version first and install only after confirming availability. Check DSH, Node and a complete Cordis combination listed above, stop the target Web Profile Host, then install the plugin. A verified local tarball can also be installed using the next section: ```sh -npm view @astron-team/skillhub-dsh@0.6.1 version -dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.1 --ignore-scripts +npm view @astron-team/skillhub-dsh@0.6.2 version +dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.2 --ignore-scripts dsh --profile skillhub-test --host 127.0.0.1 --port 0 --no-open ``` @@ -274,4 +276,8 @@ Manual update actions on conversation cards also open the change preview. Model The bookmark on market cards and details saves a local favorite. The browse view's “仅看收藏” filter reads the complete current-source list and searches it locally, independently of loaded market pages. Favorites require no login, install nothing, and do not change the website's star count. Opening an item resolves its current downloadable version; offline or missing items stay saved without automatically installing an old version. -Favorites use `$DSH_HOME/skillhub/favorites.json`, with DSH_HOME resolved by the Host's path rules. Profiles and projects sharing that home share favorites; Registry sources and different DSH_HOME values remain isolated. Removing a favorite does not uninstall a skill, and switching or removing a source does not delete its favorites. Read/write failures are reported without silently clearing the list or overwriting unknown formats. This describes the source candidate, not the published npm `0.6.1` package. +Favorites use `$DSH_HOME/skillhub/favorites.json`, with DSH_HOME resolved by the Host's path rules. Profiles and projects sharing that home share favorites; Registry sources and different DSH_HOME values remain isolated. Removing a favorite does not uninstall a skill, and switching or removing a source does not delete its favorites. Read/write failures are reported without silently clearing the list or overwriting unknown formats. This is part of the `0.6.2` candidate, not the published npm `0.6.1` package; verify that the target version is publicly available first. + +After a successful removal, a dismissible top-right notice names the skill and can undo the latest removal. Failed undo retains its retry action. Leaving the page, switching sources, or manually saving that skill again clears the old notice. Empty search results offer a clear-search action. A new search cancels an unfinished open request so late results cannot override the new intent. + +Successful writes use the complete list returned by the Host without an immediate extra read. Filtering and searching add no network requests. Re-entry, window focus, and reconnection can refresh changes from another Host; there is no polling or real-time multi-window push. diff --git a/plugins/dsh/README.md b/plugins/dsh/README.md index d88e17d30..b879f693c 100644 --- a/plugins/dsh/README.md +++ b/plugins/dsh/README.md @@ -14,6 +14,8 @@ SkillHub 的 DSH 原生插件。在 DSH 侧栏打开 **技能中心**,匿名 `0.6.1` 包含受管理技能启停、统一只读检查更新,以及横向已安装卡片和更多菜单。共享库存继续使用 v2,不要求用户同步升级 CLI;已通过与 CLI 0.2.0 的真实旧包兼容验收。公开可用状态以 npm 为准。 +`0.6.2` 为本轮补丁候选,汇总多来源与更新隔离、升级变化预览及本地文件修改处理、单技能 ZIP 导入导出、本机收藏,以及 DSH `0.2.1-rc.5` 兼容适配。共享库存保持 v2,不要求升级 CLI。候选与公开包分别验收,公开可用状态以 npm 为准。 + ## 使用已安装技能 市场、已安装列表和对话中的日常列表均按技能展示。点击“使用技能”,由 Host 核验当前会话实际可用的唯一安装;不会让用户猜安装位置。技能中心入口在同一工作区新建对话,对话内入口保留当前会话。案例显示在原生输入框上方,确认案例或自定义任务后填入可编辑草稿;不会自动发送、调用模型或覆盖原会话草稿。 @@ -23,7 +25,7 @@ SkillHub 的 DSH 原生插件。在 DSH 侧栏打开 **技能中心**,匿名 需要当前活动会话。被项目同名技能覆盖、本地文件变更、禁止用户调用或会话/来源变化时会提示原因;历史回执不能代替当前安装状态。此功能自 `0.4.0` 起提供。 -## 本地技能包导入与导出(开发中,未发布) +## 本地技能包导入与导出(0.6.2) 在“已安装”页面点击“导入技能包”,选择根目录含 `SKILL.md` 的 ZIP。预览后选择“仅此项目”或“所有项目”,确认才会安装;已有同名目录时拒绝覆盖。首期不支持套件、多技能包或自动识别嵌套目录,压缩包最多 8 MiB,展开仍受共享安装核心的文件数和大小限制。 @@ -35,7 +37,7 @@ SkillHub 的 DSH 原生插件。在 DSH 侧栏打开 **技能中心**,匿名 | 项目 | 要求或范围 | | --- | --- | -| DSH | 源码候选支持 `0.2.0-rc.2`、`0.2.1-rc.2`、`0.2.1-rc.5`;Host、app-boot 与注入 Client 模块必须属于同一精确发行版 | +| DSH | `0.6.2` 候选支持 `0.2.0-rc.2`、`0.2.1-rc.2`、`0.2.1-rc.5`;Host、app-boot 与注入 Client 模块必须属于同一精确发行版 | | Cordis | `0.2.0-rc.2` 保留以下两组完整组合;`0.2.1-rc.2` 与 `0.2.1-rc.5` 仅使用 `4.0.5 / 1.0.6 / 1.0.10 / 1.1.7` | | Host Node | `>=24.2.0 <25`;不能使用 Node 24.0.0 | | 开发包管理器 | Bun `1.3.13`,按锁文件安装 | @@ -47,7 +49,7 @@ Windows 浏览器验证不等于 Windows 原生 Host 的全部文件操作已经 `0.3.1` 包含已合并的 [Issue #981 兼容修复](https://github.com/iflytek/skillhub/pull/988);已发布的 `0.3.0` 不包含此修复。其他 RC、未来发行版、混装模块和 Host/Client 版本不一致均拒绝新写入。可选 `userQuestions` 按宿主服务是否可用启用,缺失时仍回退插件确认框;其版本不属于必需写入门禁。三个已列 RC 的问答 API 一致,peer 声明允许这些精确版本,不据此承诺其他版本兼容。 -源码候选新增 `0.2.1-rc.5` 的支持范围与验收按 [兼容设计](../../openspec/changes/add-dsh-native-plugin/compatibility-design.md) 及任务第53节的验证记录维护;新增版本不表示公开 npm `0.6.1` 已具备同等支持。平台、Node边界、浏览器与模型验收分别记录,发布另行安排。 +`0.6.2` 候选新增 `0.2.1-rc.5` 的支持范围与验收按 [兼容设计](../../openspec/changes/add-dsh-native-plugin/compatibility-design.md) 及任务第53节的验证记录维护;新增版本不表示公开 npm `0.6.1` 已具备同等支持。平台、Node边界、浏览器与模型验收分别记录,发布另行安排。 ## 构建与打包 @@ -88,8 +90,8 @@ npm pack --ignore-scripts --pack-destination /absolute/path/to/candidate-output 先查询 npm 上的精确版本,确认可用后安装。核对 DSH、Node 和上表完整 Cordis 组合,停止目标 Web Profile 的 Host,再执行插件安装;也可按下一节安装已验证的本地 tarball: ```sh -npm view @astron-team/skillhub-dsh@0.6.1 version -dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.1 --ignore-scripts +npm view @astron-team/skillhub-dsh@0.6.2 version +dsh plugin --profile skillhub-test add @astron-team/skillhub-dsh@0.6.2 --ignore-scripts dsh --profile skillhub-test --host 127.0.0.1 --port 0 --no-open ``` @@ -277,4 +279,8 @@ dsh plugin --profile skillhub-test remove @astron-team/skillhub-dsh 市场卡片与详情中的书签按钮用于本机收藏;浏览页的“仅看收藏”读取当前来源的完整收藏列表,可直接搜索,不依赖已加载的市场分页。收藏不安装技能、不改变网站收藏数,也不需要登录。点击收藏项时重新解析当前可下载版本;离线或下架不删除收藏,也不自动安装旧版本。 -收藏保存在 `$DSH_HOME/skillhub/favorites.json`(DSH_HOME按宿主路径规则解析),同一DSH_HOME下的Profile和项目共享,来源之间隔离。取消收藏不会卸载,切换或移除来源不会删除其收藏;不同DSH_HOME不自动同步。文件异常或保存失败会提示重试,不静默清空;未知格式不覆盖。此处描述源码候选能力,不表示公开npm `0.6.1` 已包含该功能。 +收藏保存在 `$DSH_HOME/skillhub/favorites.json`(DSH_HOME按宿主路径规则解析),同一DSH_HOME下的Profile和项目共享,来源之间隔离。取消收藏不会卸载,切换或移除来源不会删除其收藏;不同DSH_HOME不自动同步。文件异常或保存失败会提示重试,不静默清空;未知格式不覆盖。此功能纳入 `0.6.2` 候选,不属于公开 npm `0.6.1`;请先核对目标版本公开可用。 + +取消收藏成功后,右上角通知显示技能名称,并可撤销最近一次取消;撤销失败保留重试入口。通知可手动关闭,离开页面或切换来源后失效,手动重新收藏同项也会清除旧通知。收藏搜索无匹配时可直接清空条件;提交新搜索会取消尚未完成的打开请求,迟到结果不会打断新搜索。 + +收藏成功直接采用 Host 返回的完整列表,不立即重复读取;筛选与搜索不追加网络请求。进入页面、窗口重新聚焦或重连时可刷新以发现其他 Host 修改,没有定时轮询或实时多窗口推送。 diff --git a/plugins/dsh/package.json b/plugins/dsh/package.json index 0a0bd12b1..884c5ac48 100644 --- a/plugins/dsh/package.json +++ b/plugins/dsh/package.json @@ -1,6 +1,6 @@ { "name": "@astron-team/skillhub-dsh", - "version": "0.6.1", + "version": "0.6.2", "private": false, "type": "module", "license": "Apache-2.0", diff --git a/plugins/dsh/src/client/catalog-page.tsx b/plugins/dsh/src/client/catalog-page.tsx index 6756d79e3..d41d759f1 100644 --- a/plugins/dsh/src/client/catalog-page.tsx +++ b/plugins/dsh/src/client/catalog-page.tsx @@ -12,10 +12,11 @@ import { UiAvailabilityNotice, type UiAvailability } from './ui-availability.js' import { NativeControlStyles } from './native-control-styles.js'; import { catalogErrorMessage } from './catalog-error.js'; import { FavoriteButton } from './favorite-button.js'; -import { favoriteIdentity, filterFavorites, resolvedFavoriteItem } from './favorites.js'; +import { favoriteIdentity, favoriteSnapshot, filterFavorites, resolvedFavoriteItem } from './favorites.js'; import { useFavorites } from './use-favorites.js'; import { QueryClient, QueryClientProvider, useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'; import { useLayoutEffect, useRef, useState, useSyncExternalStore } from 'react'; +import { createPortal } from 'react-dom'; import type { TypertRemoteNamespaceMap } from '@deepseek-ai/dsh-typert-protocol'; import { installedNavigationFor, type InstalledNavigation } from './installed-navigation.js'; import { InstalledSkills } from './installed-skills.js'; @@ -70,6 +71,9 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa const [favoriteDraft, setFavoriteDraft] = useState(navigation.favoriteDraft ?? ''); const [favoriteOpening, setFavoriteOpening] = useState<{ revision: string; key: string } | null>(null); const [favoriteOpenError, setFavoriteOpenError] = useState<{ revision: string; item: FavoriteSkill; intent: 'detail' | 'install'; message: string } | null>(null); + const [favoriteUndo, setFavoriteUndo] = useState<{ revision: string; item: FavoriteSkill } | null>(null); + const favoriteFeedbackGeneration = useRef(0); + const undoPending = useRef(false); const [sort, setSort] = useState(navigation.sort ?? 'newest'); const [labelSelection, setLabelSelection] = useState(navigation.labelSelection); const [selected, setSelected] = useState(navigation.selected); @@ -84,6 +88,11 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa const compatibility = useQuery({ queryKey: ['skillhub-compatibility'], queryFn: async () => unwrap(await remote.getCompatibility()), retry: false }); const revision = context.data?.sourceRevision; const latestRevision = useRef(revision); latestRevision.current = revision; + useLayoutEffect(() => { + favoriteFeedbackGeneration.current++; + setFavoriteUndo(null); + return () => { favoriteFeedbackGeneration.current++; }; + }, [revision, selected, tab]); const favorites = useFavorites(remote, revision, !!context.data?.configured && tab === 'browse'); const favoriteItems = useRef(favorites.items); favoriteItems.current = favorites.items; useLayoutEffect(() => () => { favoriteRequest.current?.abort(); favoriteRequest.current = null; }, [revision]); @@ -135,7 +144,10 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa const filterChanged = previousFavoriteOnly.current !== favoriteOnly; previousFavoriteOnly.current = favoriteOnly; if (scroller.current) scroller.current.scrollTop = selected ? 0 : favoriteOnly ? navigation.favoriteScrollTop ?? 0 : navigation.scrollTop; if (selected) heading.current?.focus(); - else if (!filterChanged) lastCard.current?.focus({ preventScroll: true }); + else if (!filterChanged) { + if (lastCard.current?.isConnected) lastCard.current.focus({ preventScroll: true }); + else if (favoriteOnly) favoriteFilter.current?.focus({ preventScroll: true }); + } }, [selected, navigation, tab, favoriteOnly]); function back() { navigation.selected = null; setSelected(null); } function rememberScroll() { @@ -179,10 +191,48 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa const pressed = favorites.items.some(saved => favoriteIdentity(saved) === favoriteIdentity(item)); return { + if (!revision) return; + const captured = revision, generation = favoriteFeedbackGeneration.current; + const focused = document.activeElement; + const removedCard = !selected && favoriteOnly && pressed; if (pressed && favoriteOpening?.key === favoriteIdentity(item)) cancelFavoriteOpen(); - void favorites.save(item, !pressed).then(saved => { if (saved && favoriteOnly && pressed) favoriteFilter.current?.focus({ preventScroll: true }); }); + void favorites.save(item, !pressed).then(saved => { + if (latestRevision.current !== captured || favoriteFeedbackGeneration.current !== generation) return; + // Reconciliation can remove the card even when the write response was lost. + if (removedCard) returnFavoriteNoticeFocus(focused, generation); + if (!saved) return; + if (pressed) { + setFavoriteUndo({ revision: captured, item: favoriteSnapshot(item) }); + setFavoriteOpenError(current => current && favoriteIdentity(current.item) === favoriteIdentity(item) ? null : current); + } else setFavoriteUndo(current => current && favoriteIdentity(current.item) === favoriteIdentity(item) ? null : current); + }); }} />; } + async function undoFavorite() { + if (!favoriteUndo || favoriteUndo.revision !== revision || !favorites.ready || favorites.saving || undoPending.current) return; + const removed = favoriteUndo, generation = favoriteFeedbackGeneration.current, focused = document.activeElement; + undoPending.current = true; + try { + if (await favorites.save(removed.item, true) && latestRevision.current === removed.revision && favoriteFeedbackGeneration.current === generation) { + setFavoriteUndo(current => current === removed ? null : current); + returnFavoriteNoticeFocus(focused, generation); + } + } finally { undoPending.current = false; } + } + function returnFavoriteNoticeFocus(focused: Element | null, generation: number) { + if (!(focused instanceof HTMLElement)) return; + requestAnimationFrame(() => { + if (favoriteFeedbackGeneration.current === generation && !focused.isConnected && document.activeElement === document.body) { + (favoriteFilter.current ?? heading.current)?.focus({ preventScroll: true }); + } + }); + } + function leaveCatalog() { + favoriteFeedbackGeneration.current++; + setFavoriteUndo(null); cancelFavoriteOpen(); + if (!selected && tab === 'browse') rememberScroll(); + onReturn(); + } function manageSkill(item: NonNullable['items'][number], intent: 'remove' | 'upgrade') { if (intent === 'remove') { if (revision && context.data) setRemovalCandidate({ item, revision, source: item.registry ?? context.data.registryUrl }); return; } if (revision && context.data) setUpgradeCandidate({ item, revision, source: item.registry ?? context.data.registryUrl }); @@ -213,6 +263,7 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa if (next === query) void listing.refetch(); } function searchFavorites(next: string) { + cancelFavoriteOpen(); navigation.favoriteQuery = next; navigation.favoriteScrollTop = 0; setFavoriteQuery(next); if (scroller.current) scroller.current.scrollTop = 0; } @@ -222,6 +273,11 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa } return
+ {favoriteUndo && favoriteUndo.revision === revision && tab === 'browse' && createPortal(, scroller.current?.closest('dialog') ?? document.body)} {upgradeCandidate && installed && setUpgradeCandidate(null)} />} {removalCandidate && installed &&
SkillHub/{selected ? '技能详情' : '技能广场'}
- {(installation || installed) && }
+ {(installation || installed) && } {sourceOpen &&

当前来源:{context.data?.registryUrl || '尚未配置来源'}

} {(compatibility.isError || compatibility.data?.dependencyBaseline === 'unverified') &&
@@ -257,10 +313,7 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa {!sourceMatches ? error('来源已变化,请返回技能广场重新选择。', back) : detail.isPending ? : detail.isError ? error(detail.error.message, () => { void detail.refetch(); }) - : <> - {installation && installed && sameRegistry(item.registry, context.data.registryUrl) && item.namespace === selected.namespace && item.slug === selected.slug) ?? []} - sourceRevision={revision!} services={installed} verified={!!inventory.data && !inventory.isError && inventory.data.complete && !inventory.data.unavailable.length} - onInstall={() => setInstallCandidate({ item: selected, revision: revision! })} />} + : <>
{ void detail.refetch(); }} cacheIdentity={JSON.stringify([installation?.sessionId ?? installed?.sessionId, revision, selected.namespace, selected.slug, selected.latestVersion])} @@ -268,6 +321,9 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa onSelectVersion={version => { const next = { ...selected, latestVersion: version }; navigation.selected = next; setSelected(next); }} />
} @@ -306,7 +362,7 @@ function Catalog({ remote, navigation, onReturn, installation, installed, availa {installed && !inventory.isFetching && (inventory.isError || !inventory.data || !inventory.data.complete || inventory.data.unavailable.length > 0) &&

部分本地安装状态暂时无法核实,安装和管理操作暂不可用。

} {!favoriteOnly && limited &&

当前来源仅支持首批搜索,不支持分页与排序。可输入更具体的关键词缩小范围。

} {favoriteOnly ? favorites.loading ? : !favorites.ready ? null : visibleFavorites.length === 0 - ?

{favorites.items.length ? '没有匹配的收藏。' : '当前来源还没有收藏。'}

{!favorites.items.length && }
+ ?

{favorites.items.length ? '没有匹配的收藏。' : '当前来源还没有收藏。'}

{favorites.items.length ? : }
:
{visibleFavorites.map(item => sameRegistry(record.registry, context.data.registryUrl) && record.namespace === item.namespace && record.slug === item.slug) ? {skillInstallationSummary(inventory.data.items.filter(record => sameRegistry(record.registry, context.data.registryUrl) && record.namespace === item.namespace && record.slug === item.slug))} : undefined} diff --git a/plugins/dsh/src/client/native-control-styles.tsx b/plugins/dsh/src/client/native-control-styles.tsx index b664226b7..998ea66f9 100644 --- a/plugins/dsh/src/client/native-control-styles.tsx +++ b/plugins/dsh/src/client/native-control-styles.tsx @@ -3,6 +3,7 @@ export function NativeControlStyles() { return