@morehao/dsh-skills-console 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -94,7 +94,7 @@ Some skills only have an English description, and some titles alone don't tell y
94
94
  The **refresh button** next to the identity capsule in the page header checks whether *Skills Console itself* has a new release. This is a different axis from skill updates — it checks this plugin's own npm releases:
95
95
 
96
96
  - **How it checks**: follows the registry plan from the host's `pluginManager.registries()` (inheriting the profile's mirror / private source / token config) and asks `<registry>/<package>/latest` in turn (8s timeout, version only; falls back to the official `registry.npmjs.org` only if none answer), then compares with the installed version; the host caches for 5 minutes so repeated opens don't hit the network. About 20 seconds after startup it warms the cache once, silently.
97
- - **One-click update**: when the desktop host exposes `pluginManager`, clicking "Update to x.y.z" reinstalls this plugin to the latest version (installed from the registry that reported it); **restart DSH to apply** (no auto-reload).
97
+ - **One-click update**: when the desktop host exposes `pluginManager`, clicking "Update to x.y.z" reinstalls this plugin to the latest version (installed from the registry that reported it). With HMR, the host refuses to overwrite an installed package by name with `already-installed`; the plugin follows the official update path (`removeBundle` then `installBundle`); **restart DSH to apply** (no auto-reload).
98
98
  - **Manual fallback**: if one-click update is unavailable, the dialog shows a copyable command (`dsh plugin --profile <profile> add <package>@latest`).
99
99
  - **When a new version exists**: a small dot lights up on the refresh button; open it to see current / latest version and the update button, plus the release's **GitHub Release notes** body pulled for that version (if unavailable, a "view release notes on GitHub" link instead).
100
100
  - **Boundaries**: it only queries registries and only (re)installs this plugin itself, touching no other plugin's state directory; failures degrade silently to "could not reach the registry" and never affect any skill-management feature.
package/README.md CHANGED
@@ -94,7 +94,7 @@ DSH(DeepSeek Harness)的技能控制台:**安装、更新、删除、启
94
94
  页头身份胶囊旁的**刷新按钮**用来检查「技能控制台」这个插件自己有没有新版本,而管的是另一个维度——它不是检查技能的上游,而是检查本插件在 npm 上的发布:
95
95
 
96
96
  - **怎么查**:按宿主 `pluginManager.registries()` 给的 registry 计划(继承 profile 的镜像 / 私有源 / token 配置)依次问 `<registry>/<包名>/latest`(8 秒超时,只取版本号,取不到才回退官方 `registry.npmjs.org`),与本机安装的版本比较;Host 端 5 分钟缓存,连续打开不重复打网络。启动后约 20 秒会静默热身一次,不打扰你。
97
- - **一键更新**:桌面端有 `pluginManager` 服务时,弹窗里点「更新到 x.y.z」即可把本插件重装到最新版(装到查到最新版的那个 registry);更新成功后**重启 DSH 生效**(不自动刷新)。
97
+ - **一键更新**:桌面端有 `pluginManager` 服务时,弹窗里点「更新到 x.y.z」即可把本插件重装到最新版(装到查到最新版的那个 registry)。HMR 开启时宿主会以 `already-installed` 拒绝覆盖同名已安装包,插件按官方更新路径先 `removeBundle` 再 `installBundle`;更新成功后**重启 DSH 生效**(不自动刷新)。
98
98
  - **手动兜底**:环境不支持一键更新时,弹窗给出可复制的手动命令(`dsh plugin --profile <profile> add <包名>@latest`)。
99
99
  - **有新版本时**:刷新按钮上会亮一个小圆点,点开即见当前版本 / 最新版本与更新按钮;弹窗还会拉取该版本在 GitHub 上的 **Release 发布说明**正文一并展示(拉不到时给「在 GitHub 查看发布说明」链接)。
100
100
  - **边界**:只问 registry、只装本插件自身,不读写其他插件的状态目录;失败静默降级为「暂时无法获取最新版本」,不影响技能管理的任何功能。
package/client.js CHANGED
@@ -5083,6 +5083,11 @@ function SkillReader({
5083
5083
 
5084
5084
  // client/views/plugin-update-modal.js
5085
5085
  var import_react9 = __toESM(require("react"), 1);
5086
+ function flattenDetails(text) {
5087
+ return String(text ?? "").replace(/<details[^>]*>\s*<summary>([\s\S]*?)<\/summary>/gi, (_, summary) => `**${String(summary).trim()}**
5088
+
5089
+ `).replace(/<\/details>/gi, "");
5090
+ }
5086
5091
  function PluginUpdateModal({ t, update, onClose, repoUrl }) {
5087
5092
  const { status, checking, applying, checkError, applyError, check, apply: apply2, release, loadRelease } = update;
5088
5093
  const [copied, setCopied] = import_react9.default.useState(false);
@@ -5101,7 +5106,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
5101
5106
  }, [available, latest, loadRelease]);
5102
5107
  const releaseData = release?.version === latest ? release.data : null;
5103
5108
  const releaseLoading = release?.version === latest && release.loading;
5104
- const releaseBody = typeof releaseData?.body === "string" ? releaseData.body.trim() : "";
5109
+ const releaseBody = typeof releaseData?.body === "string" ? flattenDetails(releaseData.body.trim()).trim() : "";
5105
5110
  const releaseUrl = releaseData?.htmlUrl || (repoUrl ? `${repoUrl}/releases` : null);
5106
5111
  const copy = () => {
5107
5112
  if (!manual || typeof navigator === "undefined" || !navigator.clipboard?.writeText) return;
@@ -5114,7 +5119,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
5114
5119
  let stateLine;
5115
5120
  if (checking && !status) stateLine = t("pluginUpdate.checking");
5116
5121
  else if (checkError || status?.checkFailed) stateLine = t("pluginUpdate.checkFailed");
5117
- else if (restartRequired) stateLine = t("pluginUpdate.updated", { version: updatedVersion || latest });
5122
+ else if (restartRequired) stateLine = t("pluginUpdate.updated").replace("{version}", String(updatedVersion || latest || ""));
5118
5123
  else if (available) stateLine = t("pluginUpdate.available");
5119
5124
  else if (status) stateLine = t("pluginUpdate.upToDate");
5120
5125
  else stateLine = t("pluginUpdate.checking");
@@ -5133,7 +5138,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
5133
5138
  className: "dshsc-btn primary",
5134
5139
  disabled: applying,
5135
5140
  onClick: () => void apply2()
5136
- }, applying ? t("pluginUpdate.applying") : t("pluginUpdate.update", { version: latest })));
5141
+ }, applying ? t("pluginUpdate.applying") : t("pluginUpdate.update").replace("{version}", String(latest || ""))));
5137
5142
  }
5138
5143
  footer.push(h("button", { key: "close", className: "dshsc-btn ghost", onClick: onClose }, t("pluginUpdate.close")));
5139
5144
  return h(
@@ -5152,7 +5157,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
5152
5157
  available ? h(
5153
5158
  "div",
5154
5159
  { className: "dshsc-release" },
5155
- h("div", { className: "dshsc-release-head" }, t("pluginUpdate.whatsNew", { version: latest })),
5160
+ h("div", { className: "dshsc-release-head" }, t("pluginUpdate.whatsNew").replace("{version}", String(latest || ""))),
5156
5161
  releaseLoading ? h("p", { className: "dshsc-sub" }, t("pluginUpdate.releaseLoading")) : releaseBody ? h("div", { className: "dshsc-release-body" }, h(Markdown, { text: releaseBody })) : h(
5157
5162
  "p",
5158
5163
  { className: "dshsc-sub" },
@@ -13,9 +13,10 @@
13
13
  * 设计取舍(对齐本机同类插件 archive-manager / ds-bot 的轻量做法,但更保守):
14
14
  * - 依次问 registry 计划里的 `<base><pkg>/latest`,8s 超时,只取 `version` 字段;失败不抛,返回 null。
15
15
  * - 版本比较手写(本仓 runtime 依赖只有 tar,不引入 semver);结构非法一律判「不更新」。
16
- * - 安装只走宿主 `pluginManager.installBundle`(桌面端官方能力);拿不到就抛
17
- * `error.plugin-update.manual`,把手动命令放进 detail 让前端展示/复制。
18
- * - 不落盘、不加锁:一次点击只触发一次,`applying` 由路由层单飞守住;重启由用户决定。
16
+ * - 安装只走宿主 `pluginManager.installBundle`(桌面端官方能力);HMR 下已安装的包会被以
17
+ * `already-installed` 拒绝,此时按官方更新路径先 `removeBundle` 再 `installBundle`;拿不到
18
+ * 这些能力就抛 `error.plugin-update.manual`,把手动命令放进 detail 让前端展示/复制。
19
+ * - 不落盘、不加锁:本模块用 `applying` 单飞(进入 `apply()` 立即置位,覆盖查询 + 安装全程);重启由用户决定。
19
20
  */
20
21
  import { readFileSync } from 'node:fs';
21
22
 
@@ -101,6 +102,26 @@ function manualCommand(meta, profileName) {
101
102
  : `dsh plugin add ${spec}`;
102
103
  }
103
104
 
105
+ /**
106
+ * 宿主 `installBundle` / `removeBundle` 的结果两种形态都要接:
107
+ * 本地服务直接返回 `{ application, error? }`;远程代理可能再包一层 `{ ok, value | error }`。
108
+ */
109
+ function unwrapResult(result) {
110
+ if (
111
+ result && typeof result === 'object'
112
+ && typeof result.ok === 'boolean'
113
+ && !('application' in result)
114
+ && ('value' in result || 'error' in result)
115
+ ) {
116
+ return result.ok ? result.value : { application: 'failed', error: result.error ?? { code: 'operation-error' } };
117
+ }
118
+ return result;
119
+ }
120
+
121
+ function failureMessage(result) {
122
+ return result?.error?.diagnostic || result?.error?.message || result?.error?.code || '更新失败';
123
+ }
124
+
104
125
  /** registry 归一成 `https://host/` 形式;非法返回 null。 */
105
126
  function normalizeRegistry(value) {
106
127
  if (typeof value !== 'string' || value.trim() === '') return null;
@@ -241,33 +262,45 @@ export function createPluginUpdate({ getPluginManager, getProfileName, fetchImpl
241
262
  if (applying) {
242
263
  throw new ServiceError('error.plugin-update.busy', '插件正在更新,请稍候', null, 409);
243
264
  }
244
- const latest = await latestVersion();
245
- if (latest === null) {
246
- throw new ServiceError('error.plugin-update.check-failed', '暂时无法获取最新版本,请稍后再试', manualCommand(meta, profileName()), 503);
247
- }
248
- if (!isNewerVersion(meta.version, latest)) {
249
- return { ...view({ latest }), updatedVersion: null };
250
- }
251
- const pm = manager();
252
- if (pm === null) {
253
- throw new ServiceError(
254
- 'error.plugin-update.manual',
255
- '当前环境不支持自动更新,请手动执行命令',
256
- manualCommand(meta, profileName()),
257
- 409,
258
- );
259
- }
265
+ // 进入即置位:先查最新版也有 await,若等查到再置位,两次并发点击会都通过守卫、装两遍
260
266
  applying = true;
261
267
  try {
268
+ const latest = await latestVersion();
269
+ if (latest === null) {
270
+ throw new ServiceError('error.plugin-update.check-failed', '暂时无法获取最新版本,请稍后再试', manualCommand(meta, profileName()), 503);
271
+ }
272
+ if (!isNewerVersion(meta.version, latest)) {
273
+ return { ...view({ latest }), updatedVersion: null, applying: false };
274
+ }
275
+ const pm = manager();
276
+ if (pm === null) {
277
+ throw new ServiceError(
278
+ 'error.plugin-update.manual',
279
+ '当前环境不支持自动更新,请手动执行命令',
280
+ manualCommand(meta, profileName()),
281
+ 409,
282
+ );
283
+ }
262
284
  // 用查到最新版的那个 registry 去装,和「问」保持一致;没有就用宿主默认(不传 options)
263
285
  const registry = cache?.version === latest ? cache.registry : undefined;
264
- const result = await pm.installBundle(
265
- `${meta.name}@${latest}`,
266
- registry ? { registry } : undefined,
267
- );
286
+ const spec = `${meta.name}@${latest}`;
287
+ const installOptions = registry ? { registry } : undefined;
288
+ let result = unwrapResult(await pm.installBundle(spec, installOptions));
289
+ // HMR 开启时,installBundle 对已安装的包名会在跑 pnpm 前直接拒绝 already-installed;
290
+ // 官方页面的「更新」就是先 removeBundle 再 installBundle,这里照同一路径做显式更新。
291
+ if (result?.error?.code === 'already-installed' && typeof pm.removeBundle === 'function') {
292
+ const removed = unwrapResult(await pm.removeBundle(meta.name));
293
+ if (removed?.error || removed?.application === 'failed') {
294
+ const code = removed?.error?.code;
295
+ const message = code === 'stop-profile'
296
+ ? '插件正在运行且当前 profile 未启用 HMR,请完全退出 DSH 后手动更新'
297
+ : failureMessage(removed);
298
+ throw new ServiceError('error.plugin-update.install-failed', String(message).slice(0, 600), manualCommand(meta, profileName()), 500);
299
+ }
300
+ result = unwrapResult(await pm.installBundle(spec, installOptions));
301
+ }
268
302
  if (result?.error || !['applied', 'restart-required', 'overridden'].includes(result?.application)) {
269
- const message = result?.error?.diagnostic || result?.error?.message || result?.error?.code || '更新失败';
270
- throw new ServiceError('error.plugin-update.install-failed', String(message).slice(0, 600), manualCommand(meta, profileName()), 500);
303
+ throw new ServiceError('error.plugin-update.install-failed', String(failureMessage(result)).slice(0, 600), manualCommand(meta, profileName()), 500);
271
304
  }
272
305
  cache = { version: latest, expiresAt: now() + CACHE_TTL_MS, registry };
273
306
  return {
@@ -275,6 +308,7 @@ export function createPluginUpdate({ getPluginManager, getProfileName, fetchImpl
275
308
  updateAvailable: false,
276
309
  updatedVersion: latest,
277
310
  restartRequired: true,
311
+ applying: false,
278
312
  };
279
313
  } finally {
280
314
  applying = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morehao/dsh-skills-console",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。A skill manager for DeepSeek Harness: install, update, remove, link and toggle agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "morehao <morehao@qq.com>",