@jntm114514/market-lens 0.8.0 → 0.16.1
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.md +648 -1
- package/client.js +480 -46
- package/cordis.patch.yml +1 -1
- package/index.js +651 -19
- package/lib/activation.js +213 -0
- package/lib/button-state.js +141 -0
- package/lib/catalog.js +37 -1
- package/lib/exempt.js +234 -0
- package/lib/health.js +39 -0
- package/lib/install-codes.js +191 -0
- package/lib/postinstall.js +131 -0
- package/lib/requires-config.js +310 -0
- package/lib/source-cache.js +177 -0
- package/lib/source-config.js +17 -0
- package/lib/store-guard.js +217 -0
- package/lib/util.js +40 -2
- package/package.json +15 -7
package/README.md
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
-
# DSH
|
|
1
|
+
# DSH 插件市场雷达(npm: @jntm114514/market-lens)
|
|
2
2
|
|
|
3
3
|
把多个 DSH 插件市场聚合成一张可像 Excel 一样筛选、可拖动列宽的表,并用 npm 元数据判断插件是否适配当前 DSH 运行时。
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
+
## 安装
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @jntm114514/market-lens
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
装进 DSH 之后还要让 profile 认识它:
|
|
14
|
+
|
|
15
|
+
1. 把包装进当前 profile(DSH 插件管理器安装,或直接 `npm i` 到 profile 目录);
|
|
16
|
+
2. profile 的 `package.json` 里 `dsh.profile.bundles` 加上 `"@jntm114514/market-lens"`;
|
|
17
|
+
3. 重启 DSH,左侧栏会出现「市场雷达」入口。
|
|
18
|
+
|
|
19
|
+
> 仓库里的 `package.json` 写的是 `@local/market-lens` —— 那是**本地开发身份**(符号链接安装用)。
|
|
20
|
+
> 往 registry 发布时由发布工具在**临时副本**里改名,并同步 `cordis.patch.yml` 的 `name:` 与
|
|
21
|
+
> `client.js` 的 `__ModuleLoader__` `id`;源码里这两个值保持本地身份不变。
|
|
22
|
+
|
|
7
23
|
## 它解决什么
|
|
8
24
|
|
|
9
25
|
DSH 官方 Community Market 同一时刻只浏览一个目录源;社区里还有 dsh.so、dshmarketplace 等独立市场。各家的字段并不一致:
|
|
@@ -45,6 +61,637 @@ DSH Desktop 已经内置了两个市场能力,先说清楚,避免重复造
|
|
|
45
61
|
|
|
46
62
|
一句话:**内置市场是“商店”,本插件是“比价表 + 直达安装”**。要按用户数、评分、更新时间、DSH 版本横切比较,导出 CSV,盯着“这周新上了哪些”,或者比较完直接点「装」,都用雷达。
|
|
47
63
|
|
|
64
|
+
## 0.16.1:storeDir 是基础路径,不是完整路径
|
|
65
|
+
|
|
66
|
+
### 事故
|
|
67
|
+
|
|
68
|
+
0.16.0 修了 store 不一致,但**修错了地方**:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
# 0.16.0 写的(错)
|
|
72
|
+
storeDir: C:/Users/Shinelon/AppData/Local/pnpm/store/v11
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
于是 pnpm 报:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
pnpm now wants to use the store at "...\store\v11\v11"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**双层的 `v11/v11`** —— 因为 pnpm 的 `storeDir` 语义是**基础路径,会自己追加版本子目录**。
|
|
82
|
+
|
|
83
|
+
官方文档([pnpm.io/settings/store](https://pnpm.io/settings/store))写得很清楚:
|
|
84
|
+
|
|
85
|
+
> `storeDir` 默认 `~/AppData/Local/pnpm/store`(**不含版本号**)
|
|
86
|
+
> 版本目录由 pnpm 在该路径下建立
|
|
87
|
+
|
|
88
|
+
正确写法:
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
storeDir: C:/Users/Shinelon/AppData/Local/pnpm/store
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 这一轮的三个修正
|
|
95
|
+
|
|
96
|
+
**1. `storeDir` 改回基础路径**(`next` profile 已实际修复)
|
|
97
|
+
|
|
98
|
+
**2. 路径比较加归一化**
|
|
99
|
+
|
|
100
|
+
修完之后 `pnpm store path` 输出 `.../store\v11`(混合分隔符),而 `.modules.yaml` 记的是 `.../store/v11` —— 字符串不同、实为同一目录。不做归一就会**假报不一致**。现在统一把 `/` 归成 `\`、去掉末尾反斜杠、再 `resolve` + 小写比较。
|
|
101
|
+
|
|
102
|
+
**3. 新增 `inspectStoreDirConfig`:配置写错本身要报**
|
|
103
|
+
|
|
104
|
+
即便当下记录与探测碰巧一致,只要 `storeDir` 写成带版本目录的形式,**下次 pnpm 重算就会错位**。所以提前报,而不是等它坏:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
storeDir 写成了带版本目录的形式(.../store/v11)。pnpm 的 storeDir 是基础路径,
|
|
108
|
+
它会自己追加版本子目录,于是实际用的是 .../store/v11\v11 之类的双层路径
|
|
109
|
+
—— 与 node_modules 记录的路径不符就会报 ERR_PNPM_UNEXPECTED_STORE。
|
|
110
|
+
正确写法:storeDir: .../store
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 实测
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
next: ok 配置 .../store(基础路径) 记录/探测均 .../store\v11
|
|
117
|
+
desktop: ok 未配置 记录/探测均 .../store\v11
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
真实安装验证:`awesome-dsh-plugin` 2.6 秒装完、`storeDir` 未被改坏、随后干净卸载。孤儿目录 `store/v11/v11`(0.01 MB 空壳)已删除,主 store 完好。
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 0.16.0:store 位置不一致——重试一万次也一样
|
|
125
|
+
|
|
126
|
+
### 事故
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
[ERR_PNPM_UNEXPECTED_STORE] Unexpected store location
|
|
130
|
+
The dependencies at "...\profiles\next\node_modules" are currently linked
|
|
131
|
+
from the store at "G:\AI PGRM\POJ\.diag\pnpm-store\v11".
|
|
132
|
+
pnpm now wants to use the store at "C:\Users\Shinelon\AppData\Local\pnpm\store\v11".
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
于是**任何安装都失败**,而回执只能说"安装失败"——用户完全看不出这跟插件无关。
|
|
136
|
+
|
|
137
|
+
### 为什么会发生
|
|
138
|
+
|
|
139
|
+
pnpm 的 store 位置取决于**调用时的工作目录**,不是 `--dir`。从 `G:` 盘调用 pnpm 时,它无法把 `G:\.pnpm-store` 硬链接进 `C:` 盘的 profile,就退回 `G:\<某目录>\.pnpm-store`,并把这个绝对路径写进 `node_modules/.modules.yaml`。此后从 profile 目录调用 pnpm,探测到的是 `AppData` 那个 store,两者不符 → 每次写操作都报 `ERR_PNPM_UNEXPECTED_STORE`。
|
|
140
|
+
|
|
141
|
+
**症状延迟出现**:装的时候没事,下一次宿主调用 pnpm 才报。
|
|
142
|
+
|
|
143
|
+
### 现在:装前就能看见,装中说得清
|
|
144
|
+
|
|
145
|
+
新增 `lib/store-guard.js`,判定只需要两个数:
|
|
146
|
+
|
|
147
|
+
- `node_modules/.modules.yaml` 里记录的 `storeDir`
|
|
148
|
+
- 从 profile 目录探测到的 `storeDir`
|
|
149
|
+
|
|
150
|
+
四种结论:
|
|
151
|
+
|
|
152
|
+
| verdict | 含义 |
|
|
153
|
+
| --- | --- |
|
|
154
|
+
| `ok` | 一致(`C:/x` 与 `C:\x` 按同一路径比较,不假报) |
|
|
155
|
+
| `mismatch` | 不一致 → 任何安装必然失败 |
|
|
156
|
+
| `not-installed` | 还没有 `node_modules`,谈不上错位(不是故障) |
|
|
157
|
+
| `unknown` | 读不到记录或探测不到 → 不猜 |
|
|
158
|
+
|
|
159
|
+
install 失败时若命中 `ERR_PNPM_UNEXPECTED_STORE`,回执直接说清:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
node_modules 是按 G:\AI PGRM\POJ\.diag\pnpm-store\v11 装的,
|
|
163
|
+
但当前 pnpm 要用 C:\Users\Shinelon\AppData\Local\pnpm\store\v11
|
|
164
|
+
—— 任何安装/卸载都会报 ERR_PNPM_UNEXPECTED_STORE。这不是插件的问题。
|
|
165
|
+
|
|
166
|
+
修法(在 profile 目录里执行,cwd 必须是它):
|
|
167
|
+
1. pnpm store path # 确认当前 pnpm 用哪个 store
|
|
168
|
+
2. pnpm install # 用同一个 store 重建 node_modules
|
|
169
|
+
|
|
170
|
+
不要手改 node_modules/.modules.yaml —— 它只是记账,硬链接实际指向的还是旧 store。
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
最后那句是这轮最贵的认知:**手改记录只会骗过检查**,硬链接仍然指向旧 store,症状会在下一次安装时准确复现。
|
|
174
|
+
|
|
175
|
+
### 边界
|
|
176
|
+
|
|
177
|
+
`spawnSync('pnpm store path')` 只在**需要时**调用(安装失败、或用户主动体检),不进每次 `status`。cwd 必须是 profile —— pnpm 的 store 探测看的就是调用目录。
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 0.15.0:安装钩子——它会不会改写你的 profile
|
|
182
|
+
|
|
183
|
+
### 事故
|
|
184
|
+
|
|
185
|
+
`dsh-purge@1.1.92` 安装是**成功**的,四层体检全绿。但 pnpm 日志里有这么几行:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
node_modules/dsh-purge postinstall$ node scripts/ensure-profile-bundle.mjs
|
|
189
|
+
dsh-purge: appended to bundles (C:\Users\Shinelon\.dsh\profiles\next)
|
|
190
|
+
dsh-purge: wrote body insert (...\profiles\next\cordis.patch.yml)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
profile 的 `cordis.patch.yml` 因此多出一整段:
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
- insert:
|
|
197
|
+
- id: dsh-purge
|
|
198
|
+
name: dsh-purge
|
|
199
|
+
inject: [systemPrompt, tools] # ← 拿到了系统提示词注入权
|
|
200
|
+
config:
|
|
201
|
+
stripMnemon: true
|
|
202
|
+
capabilityProtocol: true
|
|
203
|
+
hooksDenyBypass: true
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**它改了你的 profile 配置,还拿到了 `systemPrompt` 注入权限。** 而体检什么都没说,因为安装本身确实成功了。
|
|
207
|
+
|
|
208
|
+
### 为什么 DSH 不报
|
|
209
|
+
|
|
210
|
+
宿主的 `pendingBuilds` 只在 pnpm **失败**时记录:
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
if (!succeeded) {
|
|
214
|
+
result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output)
|
|
215
|
+
throw new Error(run.output)
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
构建脚本一旦被允许、并且**跑成功**了,它改了什么没人知道。
|
|
220
|
+
|
|
221
|
+
### 现在:安装钩子单独上屏
|
|
222
|
+
|
|
223
|
+
新增 `lib/postinstall.js`,读 `package.json` 的 `scripts`,两档判据:
|
|
224
|
+
|
|
225
|
+
| verdict | 判据 | 界面含义 |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| `none` | 没有 `preinstall` / `install` / `postinstall` | 不提示 |
|
|
228
|
+
| `writes-profile` | 钩子命令里出现 `cordis.patch` / `dsh.profile` / `profiles?` / `bundles` / `package.json` | **它会改写你的 profile 配置** |
|
|
229
|
+
| `runs-build` | 有安装钩子,但看不出改 profile | **它会在你机器上执行任意代码** |
|
|
230
|
+
|
|
231
|
+
`inspectInstalledHealth` 的返回值多一个 `hooks` 字段,`install.health` 日志也记一笔。
|
|
232
|
+
|
|
233
|
+
### 一条克制:不读脚本体
|
|
234
|
+
|
|
235
|
+
只读 `package.json` 里的 `scripts` 命令文本,**不去读、不去执行**脚本体。
|
|
236
|
+
|
|
237
|
+
读脚本体要 resolve 一堆路径、处理 `node x.mjs` / `sh -c` / `&&` 链、还要防递归 —— 成本远大于收益,而命令本身(尤其 `ensure-profile-bundle.mjs` 这种名字)已经足够说明问题。
|
|
238
|
+
|
|
239
|
+
所以 `runs-build` 这一档**不装作知道它在干什么**:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
安装时会执行 postinstall: node-gyp rebuild。这类脚本能在你的机器上跑任意代码,
|
|
243
|
+
事后是否改写 profile 无法静态判断 —— 装完可以对照
|
|
244
|
+
profiles/<name>/package.json 与 cordis.patch.yml 是否变化。
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
给出自查手段,而不是给一个"安全"的假结论。
|
|
248
|
+
|
|
249
|
+
### 它不进 verdict
|
|
250
|
+
|
|
251
|
+
装了、能加载、能跑、配置齐 —— 四层全绿也照样可能在安装时改了 profile。所以 `hooks` 是**独立字段**,不参与 `verdict`,界面可以单独说这一句。
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 0.14.1:宿主返回失败结果而不是抛出——静默拒绝
|
|
256
|
+
|
|
257
|
+
### 事故
|
|
258
|
+
|
|
259
|
+
日志里明明写着**两次成功**:
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
[market-lens] install ok spec=dsh-tidy-display@0.4.7 origin=catalog 2338ms
|
|
263
|
+
[market-lens] install ok spec=dsh-tidy-display@0.4.7 origin=catalog 1730ms
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
但宿主实际输出是:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
dsh: installation rejected: Plugin dsh-tidy-display@0.4.7 is incompatible with dsh 0.2.2-rc.1
|
|
270
|
+
dsh: nothing was installed.
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
profile 里那个包一直是 `link:` 装的老副本,**什么都没变**。用户看到「已安装」,就以为装成了。
|
|
274
|
+
|
|
275
|
+
### 根因
|
|
276
|
+
|
|
277
|
+
宿主 `pluginManager` 的 `change()` 把操作异常**吞进结果对象,不向外抛**:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
catch (error) {
|
|
281
|
+
result.application = 'failed' // ← 不是 throw
|
|
282
|
+
result.error = managementError(error)
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
所以 `installBundle` / `upgrade` 会**正常 resolve** 成:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
{ application: 'failed', changed: false,
|
|
290
|
+
error: { code: 'incompatible-version', incompatible: [...] } }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
而 market-lens 只 `catch` 抛出的异常,从不看返回值的 `application` —— 于是把拒绝记成了成功。
|
|
294
|
+
|
|
295
|
+
### 修法
|
|
296
|
+
|
|
297
|
+
`install` 与 `upgrade` 两条路径都检查 `result.application === 'failed'`,命中就走失败分支:
|
|
298
|
+
|
|
299
|
+
- 日志改成 `install.rejected-by-host` / `upgrade.rejected-by-host`,不再有 `install.ok`
|
|
300
|
+
- 回 500,并带上 `layer` / `action` / `incompatible` —— 和普通失败同一套分层结论
|
|
301
|
+
- `incompatible-version` 时仍然给出 `exemption`(0.13.0 的豁免路径)
|
|
302
|
+
|
|
303
|
+
读取宿主错误对象时注意字段名:`managementError()` 给的是
|
|
304
|
+
|
|
305
|
+
| 错误类型 | 形状 |
|
|
306
|
+
| --- | --- |
|
|
307
|
+
| `ManagementFailure` | `{ code, incompatible? }` |
|
|
308
|
+
| 其它 | `{ code: 'operation-error', diagnostic }` |
|
|
309
|
+
|
|
310
|
+
**是 `diagnostic`,不是 `message`。** 读错字段会得到一个空的失败原因。
|
|
311
|
+
|
|
312
|
+
### 边界
|
|
313
|
+
|
|
314
|
+
- `application: 'restart-required'` 是**成功**(没有 HMR 时要重启才生效)
|
|
315
|
+
- 完全没有 `application` 字段的旧宿主仍按成功处理 —— 不认识的形状不能当成失败
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## 0.14.0:第五层体检——装好了,但要配置才能用
|
|
320
|
+
|
|
321
|
+
### 问题
|
|
322
|
+
|
|
323
|
+
真机案例(`@wxg-prc-cpg/dsh-weknora@0.1.0`,`dsh 0.2.2-rc.1`):
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
pnpm 日志:+ @wxg-prc-cpg/dsh-weknora 0.1.0 Done in 3.1s
|
|
327
|
+
profile:dep=0.1.0 inBundles=True node_modules 有 package.json
|
|
328
|
+
四层体检:verdict=ok 装载行 [ok] weknora → dist/index.js
|
|
329
|
+
loader 重放:skipped=[] compat veto=false
|
|
330
|
+
运行时日志:dsh-weknora: registered weknora_list_knowledge_bases,
|
|
331
|
+
weknora_search, weknora_read_document, weknora_ask
|
|
332
|
+
against http://localhost:8080/api/v1
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**装对了、加载了、跑起来了、工具全注册了。** 但 `localhost:8080` 上没有服务:
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
8080 不可达: 由于目标计算机积极拒绝,无法连接
|
|
339
|
+
WEKNORA_BASE_URL / WEKNORA_API_KEY / ... 全部未设置
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
用户看到的是「显示已安装,但没有安装成功」。这不是安装问题,是**配置缺失**。
|
|
343
|
+
|
|
344
|
+
前四层都看不见它:`fiberPhase` 是 `active`,因为"注册工具"这一步确实成功了;`active` 但功能不通和 `active` 且功能正常,在旧判定里一模一样。
|
|
345
|
+
|
|
346
|
+
### 现在:第五层,读 patch 里的配置条件
|
|
347
|
+
|
|
348
|
+
新增 `lib/requires-config.js`:解析包自带 `cordis.patch.yml` 里的 `config:` 段,抽出所有 `!!js` 表达式、里面的 `process.env.X` 引用、以及 `?? '默认值'` 回退值。
|
|
349
|
+
|
|
350
|
+
`weknora` 的真实 patch 被完整解析:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
baseUrl → WEKNORA_BASE_URL ?? 'http://localhost:8080/api/v1'
|
|
354
|
+
apiKey → WEKNORA_API_KEY (无默认值)
|
|
355
|
+
knowledgeBaseIds → WEKNORA_KNOWLEDGE_BASE_IDS ?? ''
|
|
356
|
+
agentId → WEKNORA_AGENT_ID (无默认值)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
按三种状态归类:
|
|
360
|
+
|
|
361
|
+
| status | 判据 | 含义 |
|
|
362
|
+
| --- | --- | --- |
|
|
363
|
+
| `satisfied` | 环境变量已设 | 不报 |
|
|
364
|
+
| `placeholder` | 没设,且默认值指向 localhost / 回环 / `your-x` / `<...>` | **一定会失败** → 阻断 |
|
|
365
|
+
| `missing` | 没设,也没有默认值 | 拿到 `undefined` → 提醒 |
|
|
366
|
+
| `optional` | 没设,但有正经默认值 | 能跑,只是没按你的意思配 → 提示 |
|
|
367
|
+
|
|
368
|
+
新增 verdict 档:**`needs-config`**。界面显示「装好了,但要配置才能用」,并点名哪个环境变量。
|
|
369
|
+
|
|
370
|
+
### 两条判据上的取舍
|
|
371
|
+
|
|
372
|
+
**只有 `placeholder` 算阻断。** 静态读 patch 时没法知道哪个配置项必需——`baseUrl` 缺了会退回 localhost(必然失败),但 `knowledgeBaseIds` / `agentId` 缺了往往只是少几个能力。把 `missing` 也判死,会把"只差一个可选项"的插件报成"用不了";误报一多,用户就不再相信这个提示。
|
|
373
|
+
|
|
374
|
+
**敏感值绝不回显。** 名字里带 `api-key` / `token` / `secret` / `password` / `credential` / `auth` 的项,detail 只写"(敏感值,不显示)",绝不把默认值或实际值写进响应。这些会进运行日志,而日志是要被导出分享的。
|
|
375
|
+
|
|
376
|
+
### 与 `fiberPhase` 的关系
|
|
377
|
+
|
|
378
|
+
`needs-config` 不会被 `active` 覆盖——这是两组不同的事实:
|
|
379
|
+
|
|
380
|
+
- `activated`:**而且真的跑起来了**
|
|
381
|
+
- `needs-config`:确实跑起来了,但**缺外部条件所以用不了**
|
|
382
|
+
|
|
383
|
+
混成一个,用户就分不清"要不要修"。
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## 0.13.0:兼容性拦截给一条真的能走的路
|
|
388
|
+
|
|
389
|
+
### 问题
|
|
390
|
+
|
|
391
|
+
宿主在 pnpm 装完后会复核兼容性,检出 `incompatible-version` 就**整批回滚**:
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
dsh: installation rejected: Plugin dsh-tidy-display@0.4.7 is incompatible with dsh 0.2.2-rc.1:
|
|
395
|
+
requirements {"engines.dsh":"0.1.0-rc.2 || ... || 0.2.1-rc.5"}
|
|
396
|
+
dsh: To accept this risk explicitly, grant the exact-version exemption
|
|
397
|
+
for dsh-tidy-display@0.4.7 on dsh 0.2.2-rc.1 with `dsh plugin allow-version`
|
|
398
|
+
or the plugin manager, then retry the installation or restart dsh.
|
|
399
|
+
dsh: nothing was installed.
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
宿主**给了出路**,但 market-lens 只把这条错误归到 `compat` 层、给个 `force` 提示就结束 —— 「仍然安装」是个摆设,点了还是同一个错误。
|
|
403
|
+
|
|
404
|
+
### 现在:接上宿主的 exact-version exemption
|
|
405
|
+
|
|
406
|
+
新增 `lib/exempt.js` + 两个 op:
|
|
407
|
+
|
|
408
|
+
| op | 作用 |
|
|
409
|
+
| --- | --- |
|
|
410
|
+
| `exempt.grant` | 写 profile 的 `compatibility.json`,授予精确版本豁免 |
|
|
411
|
+
| `exempt.revoke` | 撤销(用户反悔,或想恢复"一律拦截") |
|
|
412
|
+
|
|
413
|
+
落盘形状与宿主 `readProfileCompatibility` 完全一致:
|
|
414
|
+
|
|
415
|
+
```json
|
|
416
|
+
{ "dsh-tidy-display@0.4.7": ["0.2.2-rc.1"] }
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
界面侧:「仍然安装并授豁免」按钮 → 先 `exempt.grant`(带 `acceptRisk: true`,那一下点击就是明确同意)→ 立刻重试安装。
|
|
420
|
+
|
|
421
|
+
### 三条硬约束(照抄宿主的校验)
|
|
422
|
+
|
|
423
|
+
1. **key 必须是精确的 `package@version`** —— 范围 / 标签 / `latest` / `workspace:` 全部拒绝
|
|
424
|
+
2. **值必须是当前这个 DSH 的版本** —— 批准别的版本号无效(宿主自己也拒绝)
|
|
425
|
+
3. **`compatibility.json` 坏掉时不许改写** —— 改写了会冲掉用户原有内容;这种情况只报"请先手工修"
|
|
426
|
+
|
|
427
|
+
还有一条不是宿主的要求、但必须守的:**不给 `acceptRisk: true` 就不授予**。不弹确认就写豁免,等于替用户按下"我接受数据丢失风险"。
|
|
428
|
+
|
|
429
|
+
### 失败回执里的变化
|
|
430
|
+
|
|
431
|
+
`incompatible-version` 现在带回三样东西:
|
|
432
|
+
|
|
433
|
+
```json
|
|
434
|
+
"exemption": {
|
|
435
|
+
"key": "dsh-tidy-display@0.4.7",
|
|
436
|
+
"runtimeVersion": "0.2.2-rc.1",
|
|
437
|
+
"granted": false,
|
|
438
|
+
"canGrant": true,
|
|
439
|
+
"reason": null,
|
|
440
|
+
"compatibilityFile": "compatibility.json"
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
界面按 `canGrant` / `granted` 给出不同的话:能授就说"点这颗按钮会写 compatibility.json",已授就说"重启 DSH 让它重新读"。
|
|
445
|
+
|
|
446
|
+
### 注意:豁免可能要重启才生效
|
|
447
|
+
|
|
448
|
+
豁免写在 profile 目录里,宿主的 profile 是**已经加载**的。`exempt.grant` 的响应带 `restartRequired: true`,界面也会明说"如果这次仍然被拦,重启后再点一次安装"——不假装即时生效。
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## 0.12.0:ETag 确认式缓存 + fiberPhase 激活诊断
|
|
453
|
+
|
|
454
|
+
### 只有 304 才算「确认最新」
|
|
455
|
+
|
|
456
|
+
目录缓存原来是 TTL 式覆盖:每个同步周期整份重拉、整份重写 46MB 的 `catalog.json`,源站哪怕一个字都没改。
|
|
457
|
+
|
|
458
|
+
现在逐源记一份条件请求记录:
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
"sourceCaches": {
|
|
462
|
+
"dshfind": { "etag": "\"v1\"", "lastModified": null,
|
|
463
|
+
"lastConfirmedAt": "2026-11-07T...", "failureCount": 0 }
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
- 下次请求带 `If-None-Match` / `If-Modified-Since`
|
|
468
|
+
- 回 **304** → **不重写快照**,只把 `lastConfirmedAt` 推进到 now
|
|
469
|
+
- 回 **200** → 正常落盘,更新标记
|
|
470
|
+
|
|
471
|
+
**一条硬约束**:只有 304 才算确认最新。网络失败 / 5xx / 超时 / JSON 坏了 —— 一律不推 `lastConfirmedAt`,也不能静默沿用旧目录假装同步成功。过期目录对用户是错误答案,比报错更糟:他会以为"这个插件不存在",而其实是"目录没拉到"。
|
|
472
|
+
|
|
473
|
+
配套的三种 stale 原因分开说:
|
|
474
|
+
|
|
475
|
+
| reason | 含义 | 该怎么办 |
|
|
476
|
+
| --- | --- | --- |
|
|
477
|
+
| `never` | 从没成功过 | 检查源地址 |
|
|
478
|
+
| `last-sync-failed` | 上次同步失败 | 重试 |
|
|
479
|
+
| `last-confirmed-too-long-ago` | 太久没确认 | 触发一次同步 |
|
|
480
|
+
|
|
481
|
+
`status.cacheStaleness` 给出整份目录的聚合结论(`stale` / `earliestConfirmedAt` / `degraded[]`),逐源也在 `sourceHealth[]` 上带 `lastConfirmedAt` / `hasConditionalMarker` / `stale` / `staleReason`。
|
|
482
|
+
|
|
483
|
+
**304 最容易写错的一处**:`notModified` 的源 `items` 是 `null`(服务端没发 body)。如果直接 `flatMap`,那个源的条目会整体消失 → `diffRows` 判成「下架」→ 用户看到「一大批插件被下架了」,而其实只是"没变化"。现在 304 的源会从上一份目录里捞回属于它的行;一行若有本轮真拉到数据的别的源支撑,就由那一份决定,不由 304 续命。
|
|
484
|
+
|
|
485
|
+
### installed ≠ activated:fiberPhase
|
|
486
|
+
|
|
487
|
+
前三层(依赖 / 挂载 / 装载行)全对,插件仍然可能**根本没跑起来**。Cordis 给每个入口建一个 Fiber:
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
FIBER_STATE: PENDING 0 / LOADING 1 / ACTIVE 2 / FAILED 3 / DISPOSED 4 / UNLOADING 5
|
|
491
|
+
phase: pending / loading / active / failed / null / unloading
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
只有 `active` 才算真的挂上了。`failed` 时 dsh 自己的日志里也不一定有一行能看懂的报错。
|
|
495
|
+
|
|
496
|
+
判定口径照抄 `dsh-host-plugin-inventory/lib/index.js`,数据从 `ctx.loader.entries()` 读(宿主自己也是这么读的),不注入那个 remote service —— 它的注入时机不可靠。
|
|
497
|
+
|
|
498
|
+
`verdict` 现在多两档:
|
|
499
|
+
|
|
500
|
+
| verdict | 判据 | 界面 |
|
|
501
|
+
| --- | --- | --- |
|
|
502
|
+
| `activated` | 前三层全通 + fiber `active` | 绿色「安装完成」 |
|
|
503
|
+
| `installed-not-active` | 前三层全通,但 fiber 是 `failed` / `pending` / `loading` | 橙色「装好了,但没激活」+ 具体 phase |
|
|
504
|
+
| `installed-disabled` | `entry.disabled` | 橙色,说明"已禁用,不会运行" |
|
|
505
|
+
|
|
506
|
+
拿不到 `ctx.loader` 时退回静态结论 —— **不可知 ≠ 没激活**,这条边界很重要。
|
|
507
|
+
|
|
508
|
+
### 顺带
|
|
509
|
+
|
|
510
|
+
`sourceCaches` 存进 `source-config.json` 而不是 `catalog.json`:它描述的是「源的状态」而不是「目录的内容」,清缓存时不该被顺手清掉。导出配置时**不带**它(换机后旧 ETag 对新环境没意义)。
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## 0.11.0:空表三态 + 源级新鲜度上屏
|
|
515
|
+
|
|
516
|
+
### 空表不再只有一句话
|
|
517
|
+
|
|
518
|
+
0.10.0 之后表格还是只有一个空态文案 `emptyFiltered`("没有符合当前筛选条件的插件。")。于是:
|
|
519
|
+
|
|
520
|
+
- 目录**从没同步过** → 显示"没有符合筛选条件"
|
|
521
|
+
- 筛选**太狠** → 显示同一句
|
|
522
|
+
|
|
523
|
+
两句话的下一步完全不同(前者点同步、后者清筛选),用户照着错的那条做,怎么都出不来数据。
|
|
524
|
+
|
|
525
|
+
`emptyCatalog` 这个 i18n key 早在 0.6.0 就写好了,但代码 **0 次引用**。现在接上:
|
|
526
|
+
|
|
527
|
+
| 情况 | 文案 | 出口 |
|
|
528
|
+
| --- | --- | --- |
|
|
529
|
+
| 目录里确实没有行 | `emptyCatalog`「目录为空,先同步一次。」 | 空态里直接给「同步市场」按钮 |
|
|
530
|
+
| 目录有行但被筛掉了 | `emptyFiltered`「没有符合当前筛选条件的插件。」 | 工具栏的「清除筛选」 |
|
|
531
|
+
| 同步中 | 保留现有同步进度提示 | — |
|
|
532
|
+
|
|
533
|
+
判定用 `allRows`(全量)而不是当前页的行——用分页后的行去判"目录空"会把有数据误报成空。
|
|
534
|
+
|
|
535
|
+
### 源级新鲜度上屏
|
|
536
|
+
|
|
537
|
+
多源聚合下单个源失败**不会报错**,只是它那份条目少了一批。用户看到的是「这个插件搜不到」——结论错了,原因却在别处。
|
|
538
|
+
|
|
539
|
+
数据一直在 `status.sourceHealth`(`lastSuccessAt` / `failureCount` / `lastError`),但以前只在「来源」页可见,而用户排查"搜不到"时不会去那儿。
|
|
540
|
+
|
|
541
|
+
现在市场页顶部会出现一条状态带:
|
|
542
|
+
|
|
543
|
+
```
|
|
544
|
+
部分来源不可用: dsh.so · dshfind (连续失败,本节结果可能缺少它那份条目) [强制重试]
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
判定口径(严格,避免误报):
|
|
548
|
+
|
|
549
|
+
- 只有 `failureCount > 0` 或 `lastSuccessAt === null` 的源才算降级
|
|
550
|
+
- **被禁用的源不算**——那是用户自己关的,不是故障
|
|
551
|
+
- 全部健康时**什么都不渲染**(状态带只在有话说时出现)
|
|
552
|
+
- 最多点名 4 个源,其余折叠成 `+N`
|
|
553
|
+
- 同步中禁用重试按钮
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## 0.10.0:按钮状态由 Host 下发,六种语义只有一处实现
|
|
558
|
+
|
|
559
|
+
0.9.0 让失败说清楚了。这一版解决按钮本身的语义混乱:**同一行可能同时表现为「✓ 已装」和「已是最新」**,或者「更新」按钮点了只是重装同一个版本。
|
|
560
|
+
|
|
561
|
+
### 原来状态散在三处
|
|
562
|
+
|
|
563
|
+
| 位置 | 口径 |
|
|
564
|
+
| --- | --- |
|
|
565
|
+
| client `actionStateOf()` | 只看 `installed` + `update.available` |
|
|
566
|
+
| client 按钮分支 | `done && update` / `done` / `failed` 三个布尔 |
|
|
567
|
+
| i18n `installedUpToDate` | 定义了,但**代码 0 次引用** |
|
|
568
|
+
|
|
569
|
+
三者不一致,于是出现「按钮写着 ↑ 2.0.0,实际装的是 1.0.0」这类错位。
|
|
570
|
+
|
|
571
|
+
### 现在:Host 算一份 `button` 下发
|
|
572
|
+
|
|
573
|
+
`lib/button-state.js` 是唯一实现,六种稳定状态:
|
|
574
|
+
|
|
575
|
+
| state | 条件 | 按钮 | 点一下 |
|
|
576
|
+
| --- | --- | --- | --- |
|
|
577
|
+
| `install` | 未装,且折得出目标 | 「装」 | op:install,按目录版本(可降级) |
|
|
578
|
+
| `manual` | 折不出目标 | 「手动」 | 打开官方页两步指引 |
|
|
579
|
+
| `update` | 已装 + 有新版 | 「更新 ↑ x.y.z」 | op:upgrade,装到**正好**那个版本(不降级) |
|
|
580
|
+
| `current` | 已装 + registry 包 + 无新版 | 「✓ 已是最新」 | op:install 幂等重装 |
|
|
581
|
+
| `installed-local` | 已装 + link/local/github | 「✓ 已装」 | op:install 幂等重装 |
|
|
582
|
+
| `retry` | 上一次安装失败 | 「重试」(红边) | 重放上次动作 |
|
|
583
|
+
|
|
584
|
+
`publicRow` 直接下发:
|
|
585
|
+
|
|
586
|
+
```json
|
|
587
|
+
"button": { "state": "update", "mode": "upgrade", "op": "upgrade",
|
|
588
|
+
"version": "2.0.0", "labelKey": "actUpdate", "titleKey": "actUpdateHint" }
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
client 只查表渲染,不再自己推。**加一种状态只需改 Host + 补 i18n,不用碰按钮分支。**
|
|
592
|
+
|
|
593
|
+
### `current` 与 `installed-local` 为什么必须分开
|
|
594
|
+
|
|
595
|
+
- `current` = **查过、确认没有新版**
|
|
596
|
+
- `installed-local` = **根本没法比**(link / file / github / `@local/*`)
|
|
597
|
+
|
|
598
|
+
混成一个会让 link 装的包显示「已是最新」—— 而它从没查过。判定用 profile 里记的 spec:
|
|
599
|
+
|
|
600
|
+
```
|
|
601
|
+
link:G:/... → installed-local
|
|
602
|
+
github:o/r → installed-local
|
|
603
|
+
1.0.0 → 可以比 → current
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### `retry` 跟着行走,不只活在页面上
|
|
607
|
+
|
|
608
|
+
client 的 `actionBusy` 只活到下一次 `catalog` 拉取。于是「刚刚装失败」的行刷新后又变回「装」,用户以为上次成功了。
|
|
609
|
+
|
|
610
|
+
现在 Host 用内存集合 `installFailures` 记失败 key:
|
|
611
|
+
|
|
612
|
+
- `install` / `upgrade` 失败 → 标记,`button.state = 'retry'`
|
|
613
|
+
- 成功或重新开始 → 清标记
|
|
614
|
+
- 不落盘:重启后按钮回到正常状态是**对的**(重启意味着重新开始)
|
|
615
|
+
|
|
616
|
+
### 顺带修掉
|
|
617
|
+
|
|
618
|
+
- `installedUpToDate` 这个死 key 终于被 `current` 用上了
|
|
619
|
+
- `reinstall` 语义不再被折叠:`actionBusy.mode` 现在区分 `install / reinstall / upgrade`,按钮显示「重装中…」
|
|
620
|
+
- `upgradeTo` 以 Host 下发的 `button.version` 为准,不再和 `row.update.to` 各算一份
|
|
621
|
+
|
|
622
|
+
真实目录验证(25,691 行):`install 25677 · current 12 · update 2 · manual 0 · 非法状态 0`。
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## 0.9.0:安装失败要分层说清楚,别只给一坨 pnpm 原文
|
|
627
|
+
|
|
628
|
+
0.8.0 解决了"装了但不出现"。这一版解决另一件更日常的事:**失败了,但用户不知道哪一段坏了。**
|
|
629
|
+
|
|
630
|
+
### 已安装不再算失败
|
|
631
|
+
|
|
632
|
+
宿主 `dsh-plugin-manager` 有两处会抛 `already-installed`:
|
|
633
|
+
|
|
634
|
+
- HMR 下重装已知包(`Object.hasOwn(before, known)`)
|
|
635
|
+
- 装前预检 refused(path / registry 两条路径)
|
|
636
|
+
|
|
637
|
+
两处都意味着**目标状态已经达成**。旧代码把所有错误一视同仁回 500,用户看到「安装失败」,于是反复点安装——而它其实早就装好了。
|
|
638
|
+
|
|
639
|
+
现在回 200:
|
|
640
|
+
|
|
641
|
+
```json
|
|
642
|
+
{ "ok": true, "code": "already-installed", "alreadyPresent": true,
|
|
643
|
+
"note": "目标版本已经装好了(宿主判定已安装,未重新下载)",
|
|
644
|
+
"health": { "verdict": "ok", "mounted": true } }
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
升级路径同理:升到当前版本本来就是成功,不是失败。
|
|
648
|
+
|
|
649
|
+
### 13 个宿主失败码 + 10 个 pnpm 码,各归其层
|
|
650
|
+
|
|
651
|
+
这些码不是猜的,全部来自宿主源码(`dsh-plugin-manager/lib/index.js` 的 `failure.js` 与 `refused()`):
|
|
652
|
+
|
|
653
|
+
| 层 | 码 | 该给的动作 |
|
|
654
|
+
| --- | --- | --- |
|
|
655
|
+
| `state` | `already-installed` | 不是失败(回 200) |
|
|
656
|
+
| `resolve` | `ambiguous-install` / `invalid-spec` / `not-a-package` / `ERR_PNPM_PEER_DEP_ISSUES` | 选择 / 改输入 / 明确同意 |
|
|
657
|
+
| `version` | `ERR_PNPM_NO_MATCHING_VERSION` / `ERR_PNPM_FETCH_404` | 换最新版重试 |
|
|
658
|
+
| `subdir` | `ERR_PNPM_INVALID_PATH` | 复制命令 |
|
|
659
|
+
| `compat` | `incompatible-version` | 展示明细 + 「仍然安装」 |
|
|
660
|
+
| `declare` | `not-bundle` / `not-a-bundle` | 明说"这是普通依赖,装了不会有面板" |
|
|
661
|
+
| `build` | `stale-approval` / `ERR_PNPM_IGNORED_BUILDS` | 重新取 pendingBuilds 再确认 |
|
|
662
|
+
| `workspace` | `ERR_PNPM_ADDING_TO_ROOT` | 复制命令(见下) |
|
|
663
|
+
| `lock` | `EPERM` / `EBUSY` / `ETXTBSY` / `ERR_PNPM_UNEXPECTED_STORE` / `ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY` | 退避重试或复制命令 |
|
|
664
|
+
| `permission` | `management-required` / `not-removable` | 只读,不给按钮 |
|
|
665
|
+
| `activation` | `bundle-in-use` / `stop-profile` | 先禁用 / 关掉 DSH |
|
|
666
|
+
| `inventory` | `unknown-plugin` | 刷新后重试 |
|
|
667
|
+
|
|
668
|
+
回执标题现在直接说层级:
|
|
669
|
+
|
|
670
|
+
```
|
|
671
|
+
安装失败 · 版本解析层
|
|
672
|
+
ERR_PNPM_NO_MATCHING_VERSION No matching version found for foo@9.9.9
|
|
673
|
+
目录里写的版本在 npm 上不存在,可以用最新版本重试。 [改用最新版] [复制命令]
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### `ADDING_TO_ROOT` 为什么只给「复制命令」
|
|
677
|
+
|
|
678
|
+
profile 目录里确实有 `pnpm-workspace.yaml`(`packages: [.]`、`nodeLinker: hoisted`),所以 pnpm 会把 `pnpm add` 当成向 workspace root 添加,报 `ERR_PNPM_ADDING_TO_ROOT`。
|
|
679
|
+
|
|
680
|
+
但**本插件从不直接调 pnpm** —— 它调 `pluginManager.installBundle()`,宿主内部 `execa("pnpm", ...)` 且不传 `-w`。本插件没有任何参数能塞进去。所以这一条只能让用户拿去终端执行,而不是假装能自动重试。
|
|
681
|
+
|
|
682
|
+
同理,宿主已经在 `execa` 前后存 `package.json` / `pnpm-lock.yaml` 全文并在失败时 `restore()` + `pnpm install --frozen-lockfile` 回滚(第 653–675、833–870 行)。本插件**不重复实现**残缺目录隔离——那会和宿主的 restore 抢写同一批文件。
|
|
683
|
+
|
|
684
|
+
### 顺带:运行时探测适配新版 DSH 布局
|
|
685
|
+
|
|
686
|
+
新 DSH(`2.0.22-next`)的 asar 又变了:没有 `dsh/` 子目录,`dsh-app-boot` 直接在 `node_modules/@deepseek-ai/` 下。`lib/runtime.js` 现在两种布局都认:
|
|
687
|
+
|
|
688
|
+
```
|
|
689
|
+
DSH NEXT : app.asar/node_modules/@deepseek-ai/dsh-app-boot → 0.2.2-rc.1
|
|
690
|
+
desktop : app.asar/dsh/node_modules/@deepseek-ai/dsh-app-boot → 0.2.1-rc.5
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
48
695
|
## 0.8.0:安装后到底能不能加载 —— 三层装机体检
|
|
49
696
|
|
|
50
697
|
装插件最贵的问题从来不是「装不上」,而是**装上了但不出现**:点在面板上「装」,回执是绿的,重启后却没有设置页、没有面板、没有数据目录,dsh 的日志里一行都不提。0.8.0 把这条链路拆成三层,每层都查,结论一次说清。
|