dsh-toolbox-ui 1.6.4 → 1.6.5
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 +2 -2
- package/README.md +2 -2
- package/docs/installation.md +5 -5
- package/lib/skills-file-ops.js +11 -2
- package/lib/skills-registry.js +29 -44
- package/lib/whats-new.js +18 -1
- package/package.json +2 -1
package/README.en.md
CHANGED
|
@@ -57,10 +57,10 @@ See every installed skill's **real scope** and cross-scope shadowing, enable/dis
|
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
59
|
# Recommended: from npm — `dsh plugin update` then upgrades within ^1.x
|
|
60
|
-
dsh plugin --profile web add dsh-toolbox-ui@^1.6.
|
|
60
|
+
dsh plugin --profile web add dsh-toolbox-ui@^1.6.5
|
|
61
61
|
|
|
62
62
|
# Or pin a GitHub release tag (no automatic upgrades; re-add with a new tag)
|
|
63
|
-
dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.
|
|
63
|
+
dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.5
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Then **restart `dsh web`**. To uninstall (your existing MCP entries stay):
|
package/README.md
CHANGED
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
|
|
58
58
|
```sh
|
|
59
59
|
# 推荐:npm 安装 —— 之后 `dsh plugin update` 在 ^1.x 内自动升小版本
|
|
60
|
-
dsh plugin --profile web add dsh-toolbox-ui@^1.6.
|
|
60
|
+
dsh plugin --profile web add dsh-toolbox-ui@^1.6.5
|
|
61
61
|
|
|
62
62
|
# 或固定 GitHub release tag(不自动跨版本,升级要换 tag 重新 add)
|
|
63
|
-
dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.
|
|
63
|
+
dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.5
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
装完**重启 `dsh web`**。卸载(不会删除 profile 里已有的 MCP 条目):
|
package/docs/installation.md
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
推荐用 npm 安装:包里的 `^1.x` 范围让后续 `dsh plugin update` 能自动升级小版本。
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
|
-
dsh plugin --profile web add dsh-toolbox-ui@^1.6.
|
|
18
|
+
dsh plugin --profile web add dsh-toolbox-ui@^1.6.5
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
也可以固定 GitHub release tag(适合需要锁定某个具体版本时):
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
24
|
# 固定 release tag
|
|
25
|
-
dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.
|
|
25
|
+
dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#v1.6.5
|
|
26
26
|
|
|
27
27
|
# 或固定某个 commit
|
|
28
28
|
dsh plugin --profile web add github:Imzl-zl/dsh-toolbox-ui#<commit>
|
|
@@ -73,7 +73,7 @@ dsh plugin --profile web update dsh-toolbox-ui
|
|
|
73
73
|
|
|
74
74
|
| 安装规格 | `update` 的效果 | 怎么跨版本 |
|
|
75
75
|
|---|---|---|
|
|
76
|
-
| `dsh-toolbox-ui@^1.6.
|
|
76
|
+
| `dsh-toolbox-ui@^1.6.5`(npm,推荐) | 在 `^1.x` 内自动升到最新小版本,不会跨到 2.0.0 | 已是自动;跨大版本显式安装 `@^2` |
|
|
77
77
|
| `github:...#v1.4.0`(tag) | 重新解析同一个 tag,**不会动** | 换成新 tag 重新 `add`:`... #<新 tag>` |
|
|
78
78
|
| `github:...#<commit>` | 同上,固定在 commit | 用新 commit/tag 重新 `add` |
|
|
79
79
|
| `github:...#main`(分支) | 拉取最新 main | 已是自动;但不推荐生产(无 release 门槛) |
|
|
@@ -113,7 +113,7 @@ dsh web # 打开面板点一遍
|
|
|
113
113
|
# 3) 提交并推 tag(CI 会校验 tag 与 version 一致,不一致就拒绝发布)
|
|
114
114
|
git add -A && git commit -m "..."
|
|
115
115
|
git push origin main
|
|
116
|
-
git tag -a v1.6.
|
|
116
|
+
git tag -a v1.6.5 -m "dsh-toolbox-ui 1.6.5" && git push origin v1.6.5
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
推 tag 后 CI 依次做:校验版本 → `pnpm install --frozen-lockfile` → `npm test` → `npm publish`(trusted publishing,无需 token)→ `gh release create`。
|
|
@@ -132,7 +132,7 @@ git tag -a v1.6.4 -m "dsh-toolbox-ui 1.6.4" && git push origin v1.6.4
|
|
|
132
132
|
|
|
133
133
|
```sh
|
|
134
134
|
npm publish # 会弹 2FA 挑战;若用 bypass-2FA 的 granular token 则非交互
|
|
135
|
-
gh release create v1.6.
|
|
135
|
+
gh release create v1.6.5 --title "dsh-toolbox-ui v1.6.5" --notes-file <notes>
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
`npm login` 的会话 token 只有 2 小时,超时需要重新登录;classic token 已被 npm 永久吊销,能非交互发布的只剩勾了 **Bypass 2FA** 的 granular access token——但它正在被 npm 收窄(已不能做账号/包管理类操作,且 2027-01 起将失去直接发布能力),所以长期方案是上面的 trusted publishing。
|
package/lib/skills-file-ops.js
CHANGED
|
@@ -131,6 +131,12 @@ export async function setSkillEnabled(target, enabled) {
|
|
|
131
131
|
* 宿主目录看不到它们(改名后按固定路径读不到即静默跳过),所以这是面板能展示
|
|
132
132
|
* 「已停用」并提供「启用」的唯一来源。
|
|
133
133
|
*
|
|
134
|
+
* ⚠️ 「是不是目录」必须用 `stat`(跟随链接),**不能**用 `readdir` 的 `Dirent.isDirectory()`:
|
|
135
|
+
* 技能目录是指向外部同步库的符号链接/junction 时,Dirent 报的是 `isSymbolicLink() === true`
|
|
136
|
+
* 而 `isDirectory() === false`(实测)。用 Dirent 会让这类条目掉进扁平分支、既不匹配 `.md.disabled`
|
|
137
|
+
* 又拿不到 `<dir>/SKILL.md.disabled` —— **链接技能一旦停用就从面板彻底消失,再也启用不回来**。
|
|
138
|
+
* 这与 `locateSkill` 处理的是同一个根因:判据要落在物理位置(跟随链接后的真相)上。
|
|
139
|
+
*
|
|
134
140
|
* @param {{ root: string, scope: 'global'|'project' }} input
|
|
135
141
|
* @returns {Promise<object[]>}
|
|
136
142
|
*/
|
|
@@ -145,7 +151,10 @@ export async function scanDisabledSkills(input) {
|
|
|
145
151
|
}
|
|
146
152
|
const rows = []
|
|
147
153
|
for (const entry of entries) {
|
|
148
|
-
if (entry.
|
|
154
|
+
if (typeof entry.name !== 'string') continue
|
|
155
|
+
// 跟随链接判目录(见函数头说明)。stat 失败(悬空链接)就当它不是目录,继续按扁平判。
|
|
156
|
+
const isDirectory = await stat(join(root, entry.name)).then((info) => info.isDirectory()).catch(() => false)
|
|
157
|
+
if (isDirectory) {
|
|
149
158
|
// 目录捆绑:<name>/SKILL.md.disabled
|
|
150
159
|
const name = entry.name
|
|
151
160
|
if (!SKILL_NAME.test(name)) continue
|
|
@@ -156,7 +165,7 @@ export async function scanDisabledSkills(input) {
|
|
|
156
165
|
continue
|
|
157
166
|
}
|
|
158
167
|
// 扁平:<name>.md.disabled
|
|
159
|
-
if (
|
|
168
|
+
if (entry.name.endsWith(`.md${DISABLED_SUFFIX}`)) {
|
|
160
169
|
const name = entry.name.slice(0, -`.md${DISABLED_SUFFIX}`.length)
|
|
161
170
|
if (!SKILL_NAME.test(name)) continue
|
|
162
171
|
rows.push({ name, scope, root, kind: 'flat', path: join(root, entry.name), disabled: true })
|
package/lib/skills-registry.js
CHANGED
|
@@ -58,63 +58,48 @@ async function physicalDirectory(path) {
|
|
|
58
58
|
*
|
|
59
59
|
* 为什么必须推而不是问:`ctx.skills` **不暴露 roots()**(实测公开面只有
|
|
60
60
|
* registerProvider/register/list/snapshot/get/collect/...;roots() 在 filesystem provider 内部)。
|
|
61
|
-
* 能用的线索只有 `resourceBase
|
|
61
|
+
* 能用的线索只有 `resourceBase`。
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
* 扁平文件:path = `<根>/<name>.md`, resourceBase.path = `<根>` → 根 = base
|
|
63
|
+
* 判别依据是宿主的 locator **构造规则**本身(dsh-skill-filesystem 的 discoverRoot):
|
|
65
64
|
*
|
|
66
|
-
*
|
|
65
|
+
* 目录捆绑:locator.path = `<技能目录>/SKILL.md`,locator.directory = `<技能目录>`
|
|
66
|
+
* 扁平文件:locator.path = `<根>/<name>.md`, locator.directory = `<根>`
|
|
67
67
|
*
|
|
68
|
-
* ⚠️
|
|
69
|
-
*
|
|
68
|
+
* ⚠️ 关键:**形态由技能文件名决定**(`SKILL.md` vs `<name>.md`),而**根由 `resourceBase` 决定**。
|
|
69
|
+
* 不要去比较两个字段的路径——它们来自不同的路径口径,链接下必然分叉(真机缺陷,issue #5):
|
|
70
|
+
* `resourceBase.path` = `locator.directory` —— **展示路径**(配置原样,配置写链接就是链接)
|
|
70
71
|
* `path` = `fs.processPath(target)` —— **realpath**(fs-local 里就是 targetKey)
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
* 之前两版实现都栽在这里:先按字面比较,再改成「realpath 规范化后比较」——都还是**路径比较**,
|
|
73
|
+
* 于是「技能目录是链接」修好了,「**扁平技能的文件本身是链接**」照样失效(宿主自己的用例就钉着
|
|
74
|
+
* 这两种形状:skill-filesystem.spec.ts 的 linked-dir 与 linked-flat)。而符号链接指向外部同步库
|
|
75
|
+
* 正是本仓库用户的常见做法。
|
|
74
76
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
+
* 改名判据之后:文件名在两种口径下**完全一致**(realpath 不改变 basename),所以链接、别名、
|
|
78
|
+
* Windows 大小写规范化都不影响判别,一次覆盖全部形态,也不需要碰磁盘。
|
|
77
79
|
*
|
|
78
|
-
*
|
|
80
|
+
* 返回的 root 一律用**展示路径**(`resolveTargetReference` 要求它与 skillTargetsView 算出的
|
|
81
|
+
* 目标精确相等,返回 realpath 会让所有文件操作对不上)。
|
|
82
|
+
* 认不出形态就返回空:宁可不填,也不猜一个根——猜错会让启停/删除作用在错误的目录上。
|
|
79
83
|
*
|
|
80
84
|
* @param {object} summary - 宿主 SkillSummary。
|
|
81
|
-
* @returns {
|
|
85
|
+
* @returns {{ root?: string, kind?: 'bundle'|'flat' }}
|
|
82
86
|
*/
|
|
83
|
-
|
|
87
|
+
function locateSkill(summary) {
|
|
84
88
|
const base = summary?.resourceBase?.path
|
|
85
89
|
const path = summary?.path
|
|
86
90
|
if (typeof base !== 'string' || base === '' || typeof path !== 'string' || path === '') return {}
|
|
87
91
|
|
|
88
|
-
//
|
|
89
|
-
const
|
|
90
|
-
const pathSlashes = toSlashes(path)
|
|
91
|
-
|
|
92
|
-
// 先按字面比一次:**没有链接时两个字段本来就同源**,这一步就命中,不必碰磁盘
|
|
93
|
-
// (技能列表每次刷新都要走这里,几十条技能各多一次 realpath 是白花的)。
|
|
94
|
-
const literal = matchLocator(base, baseSlashes, pathSlashes)
|
|
95
|
-
if (literal !== undefined) return literal
|
|
96
|
-
|
|
97
|
-
// 字面没命中:可能是两个字段口径不同(链接目录)。把 base 规范化到真实路径再比一次。
|
|
98
|
-
// 只在**这一条**上付 realpath 的代价,而且这一步同时兜住 Windows 上 realpath 会
|
|
99
|
-
// 规范化盘符/目录名大小写的情形——那种情况下字面同样不相等,但物理上是同一个目录。
|
|
100
|
-
const physical = await physicalDirectory(base)
|
|
101
|
-
if (physical === baseSlashes) return {} // 规范化前后一样,说明字面失败不是口径问题:不猜
|
|
102
|
-
return matchLocator(base, physical, pathSlashes) ?? {}
|
|
103
|
-
}
|
|
92
|
+
// 文件名取自宿主给的 realpath,只看最后一段(分隔符两种都要吃)。
|
|
93
|
+
const file = toSlashes(path).split('/').pop() ?? ''
|
|
104
94
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*/
|
|
114
|
-
function matchLocator(base, baseCompared, pathSlashes) {
|
|
115
|
-
if (pathSlashes === `${baseCompared}/${BUNDLE_FILE}`) return { root: dirname(base), kind: 'bundle' }
|
|
116
|
-
if (dirname(pathSlashes) === baseCompared) return { root: base, kind: 'flat' }
|
|
117
|
-
return undefined
|
|
95
|
+
// 扁平优先:`<根>/<name>.md`,用**技能名**核对文件名——这是精确判别式,
|
|
96
|
+
// 也是扁平技能的文件本身是链接时唯一还成立的那条线索。
|
|
97
|
+
if (file === `${summary.name}.md`) return { root: base, kind: 'flat' }
|
|
98
|
+
// 目录捆绑:`<技能目录>/SKILL.md`。目录名可能与技能名不同(宿主按 frontmatter 取名),
|
|
99
|
+
// 所以这里不看名字,只看那个固定文件名。
|
|
100
|
+
if (file === BUNDLE_FILE) return { root: dirname(base), kind: 'bundle' }
|
|
101
|
+
// 两种 locator 都不符:不猜。
|
|
102
|
+
return {}
|
|
118
103
|
}
|
|
119
104
|
|
|
120
105
|
/** 取宿主 skills 服务;宿主的服务名就是 'skills'(dsh-skill 的 super(ctx, "skills"))。 */
|
|
@@ -210,7 +195,7 @@ async function invocationOf(row) {
|
|
|
210
195
|
* @returns {Promise<object>} 面板行。
|
|
211
196
|
*/
|
|
212
197
|
async function toView(summary, { cwd }) {
|
|
213
|
-
const located =
|
|
198
|
+
const located = locateSkill(summary)
|
|
214
199
|
const scope = scopeOfSource(summary.source)
|
|
215
200
|
// 调用权限**不读** summary.invocation:那一份来自宿主的发现缓存(见 invocationOf 的说明)。
|
|
216
201
|
const invocation = await invocationOf({ name: summary.name, root: located.root, scope })
|
package/lib/whats-new.js
CHANGED
|
@@ -214,7 +214,7 @@ export const WHATS_NEW = [
|
|
|
214
214
|
version: '1.6.4',
|
|
215
215
|
items: [
|
|
216
216
|
{
|
|
217
|
-
text: '**技能目录是符号链接时不再「整行不可用」**:这种情况下面板以前显示「位置(未报告)/调用权限未知」,点开还读不出正文——悬浮提示把它说成 frontmatter
|
|
217
|
+
text: '**技能目录是符号链接时不再「整行不可用」**:这种情况下面板以前显示「位置(未报告)/调用权限未知」,点开还读不出正文——悬浮提示把它说成 frontmatter 有问题,其实是面板没能定位到那个目录(文件一直是好的)。现在链接目录照常定位,位置与调用权限都正常显示,读正文与「模型自动调用」开关也可用。⚠️ 同一形态下的**停用/删除**当时还没修好,见 1.6.5',
|
|
218
218
|
where: '技能列表与详情',
|
|
219
219
|
},
|
|
220
220
|
{
|
|
@@ -227,4 +227,21 @@ export const WHATS_NEW = [
|
|
|
227
227
|
},
|
|
228
228
|
],
|
|
229
229
|
},
|
|
230
|
+
{
|
|
231
|
+
version: '1.6.5',
|
|
232
|
+
items: [
|
|
233
|
+
{
|
|
234
|
+
text: '**修好「停用一条技能后它就从面板消失」**:技能目录是指向外部同步库的符号链接时,停用后那条技能既不在「已启用」也不在「已停用」里——磁盘上其实改名成功了,但面板再也看不到它、也点不到「启用」把它恢复。现在停用后照常列在「已停用」里,一键就能启用回来',
|
|
235
|
+
where: '技能列表(停用/启用)',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
text: '**链接技能的「删除」不再报错**:同样的链接形态下,删除以前会失败并报一个目标引用错误,用户以为技能删不掉。现在删除正常工作,而且**只删掉那个链接**——技能本体在外部同步库里的内容一个字都不会动',
|
|
239
|
+
where: '技能详情的「删除技能」',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
text: '**补齐「扁平技能的文件本身是链接」这种形态**:技能可以是 `<名字>.md` 单文件(而不是目录),这个文件同样可能是指向外部库的链接;上一版只修好了「目录是链接」,这一种仍会定位不到。现在四种形态(目录/单文件 × 普通/链接)都能正确定位,位置、调用权限、启停、删除、读正文全部可用',
|
|
243
|
+
where: '技能列表与详情',
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
230
247
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-toolbox-ui",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
4
4
|
"description": "Responsive MCP management UI and skills manager for DeepSeek Harness Web — floating panel, JSON import, profile-backed Host Remote management, and a skill market with a hard license gate.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"@deepseek-ai/dsh-mcp-client": "^0.2.0-rc.2",
|
|
57
57
|
"@deepseek-ai/dsh-scope": "^0.2.0-rc.2",
|
|
58
58
|
"@deepseek-ai/dsh-skill": "^0.2.0-rc.2",
|
|
59
|
+
"@deepseek-ai/dsh-skill-filesystem": "0.2.0-rc.2",
|
|
59
60
|
"@deepseek-ai/dsh-tools": "^0.2.0-rc.2",
|
|
60
61
|
"@deepseek-ai/dsh-typert-loader": "^0.2.0-rc.2",
|
|
61
62
|
"@deepseek-ai/dsh-typert-protocol": "^0.2.0-rc.2"
|