@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 +1 -1
- package/README.md +1 -1
- package/client.js +9 -4
- package/lib/core/plugin-update.js +59 -25
- package/package.json +1 -1
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
|
|
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"
|
|
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"
|
|
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"
|
|
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
|
-
* `
|
|
18
|
-
* -
|
|
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
|
-
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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.
|
|
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>",
|