@noob-stupid/dsh-plugin-console 0.5.39 → 0.5.41
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 +37 -1
- package/README.zh.md +26 -0
- package/lib/server/domain/install-finalize.js +45 -0
- package/lib/server/domain/install-job.js +50 -39
- package/lib/server/domain/install.js +8 -3
- package/lib/server/domain/link-self-sufficiency.js +417 -0
- package/lib/server/domain/manifest.js +135 -15
- package/lib/server/domain/persist.js +37 -4
- package/lib/server/domain/plugin-actions.js +67 -4
- package/lib/server/domain/selfupdate.js +9 -2
- package/lib/server/domain/source-spec.js +212 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Then restart the dsh service → refresh the page → **Settings → Plugins →
|
|
|
52
52
|
[](https://www.npmjs.com/package/@noob-stupid/dsh-plugin-console)
|
|
53
53
|
[](https://www.npmjs.com/package/@noob-stupid/dsh-plugin-console)
|
|
54
54
|
[](https://github.com/Noob-stupid/dsh-plugin-gating-hub/releases)[](https://www.dsh.so/artifact/dsh-plugin-hub)
|
|
55
|
-
[](https://www.dsh.so/artifact/dsh-plugin-hub)
|
|
55
|
+
[](https://www.dsh.so/artifact/dsh-plugin-hub/)
|
|
56
56
|
|
|
57
57
|
<img width="1170" height="609" alt="DSH Plugin Gating Hub" src="https://github.com/user-attachments/assets/b802d606-14ba-4151-9956-ff642ed12b0a" />
|
|
58
58
|
|
|
@@ -301,6 +301,42 @@ Environment reminder: an accelerator/proxy that rewrites hosts makes the direct
|
|
|
301
301
|
(see above), and free mirrors are unreliable for the **git protocol** on large packs while
|
|
302
302
|
**archive/raw are usually much faster** — the console already switches by itself.
|
|
303
303
|
|
|
304
|
+
#### Development installs (`link:`)
|
|
305
|
+
|
|
306
|
+
Ordinary installs go through the package manager (`pnpm add`); that is the baseline, not a feature here.
|
|
307
|
+
|
|
308
|
+
`link:` is the separate, **explicit** route: mount a **local directory / plugin you are developing**
|
|
309
|
+
directly into the profile.
|
|
310
|
+
|
|
311
|
+
- **Source edits take effect immediately**: `node_modules/<name>` in the profile is a **link** to your
|
|
312
|
+
local directory, not a copy — edit the source, refresh the page. No reinstall, no repackaging.
|
|
313
|
+
- **Who it is for**: plugin authors, or anyone who wants to edit a plugin's source. If you just want
|
|
314
|
+
to *use* a plugin, an ordinary install is simpler.
|
|
315
|
+
- **What it pins**: the local artifact is written into the profile manifest `dependencies`
|
|
316
|
+
(`link:<absolute path>`) and gets a matching entry in `pnpm-lock.yaml` — so lockfile health checks
|
|
317
|
+
and rebuilds never treat it as an orphan, and it **survives restarts and instance updates**.
|
|
318
|
+
- **What the console completes for it** (otherwise the usual outcome is "installed, but it will not load"):
|
|
319
|
+
- **the target directory's own `dependencies`**: one install run inside that directory, filling in
|
|
320
|
+
what the local directory is missing;
|
|
321
|
+
- **the framework peer shim root**: when the local directory lives **outside** the profile tree,
|
|
322
|
+
Node's resolution chain never reaches the profile's `node_modules`. The console links the framework
|
|
323
|
+
peers it **declares and currently cannot resolve** (`@deepseek-ai/*`) at the **plugin root**,
|
|
324
|
+
pointing at `~/.dsh/profiles/node_modules/@deepseek-ai/<pkg>`. It is a **link**, not a copy —
|
|
325
|
+
both sides share the same realpath, so Cordis service classes and LLM error classes keep their
|
|
326
|
+
**module identity shared with the host**.
|
|
327
|
+
- **Boundaries** (always reported honestly, never faked):
|
|
328
|
+
- moving or deleting the local directory **breaks** the link (the manifest `link:` then points at a
|
|
329
|
+
path that no longer exists);
|
|
330
|
+
- shims only fill peers it **declares** and that are genuinely missing; a package the framework side
|
|
331
|
+
does not have is never invented;
|
|
332
|
+
- anything **already present** at the target path (including a dangling link) is **never overwritten**
|
|
333
|
+
— it is skipped and reported;
|
|
334
|
+
- **uninstall removes the shims the console created** (only its own; links you placed by hand are
|
|
335
|
+
never touched);
|
|
336
|
+
- if completion fails (target directory not writable, dependency install failed, …) the **install
|
|
337
|
+
still completes**, and the result honestly says "not ready + what is missing" together with an
|
|
338
|
+
action you can run.
|
|
339
|
+
|
|
304
340
|
### Static index market (plugin & skill tabs)
|
|
305
341
|
|
|
306
342
|
> **Hybrid architecture**: browsing uses the static index (instant, zero GitHub API calls),
|
package/README.zh.md
CHANGED
|
@@ -236,6 +236,32 @@ git clone https://github.com/Noob-stupid/dsh-plugin-gating-hub /tmp/dsh-plugin-c
|
|
|
236
236
|
环境提醒:加速器/代理改 hosts 会让**直连源**失效(见上);免费镜像的 **git 协议**对大 pack 不可靠,
|
|
237
237
|
而 **archive/raw 通常更快** —— 控制台已按这两条自行切换,不用手动换源。
|
|
238
238
|
|
|
239
|
+
#### 开发式安装(`link:`)
|
|
240
|
+
|
|
241
|
+
普通安装走包管理器(`pnpm add`)—— 那是基线,不是这里的卖点。
|
|
242
|
+
|
|
243
|
+
`link:` 是**显式**的另一条路:把**本地目录 / 正在开发的插件**直接挂进 profile。
|
|
244
|
+
|
|
245
|
+
- **改源码即时生效**:profile 里的 `node_modules/<包名>` 是一条指向本地目录的**链接**,不是副本 ——
|
|
246
|
+
改完源码刷新页面即可,无需重装、无需重新打包。
|
|
247
|
+
- **适合谁**:插件作者,或想直接改某个插件源码的人。只想"装个插件来用"的话,普通安装更省心。
|
|
248
|
+
- **它钉住什么**:本地那份物会被写进 profile 清单的 `dependencies`(`link:<绝对路径>`),并在
|
|
249
|
+
`pnpm-lock.yaml` 里留下对应条目 —— 所以「依赖锁体检 / 重建」不会把它当多余项清掉,
|
|
250
|
+
**重启与实例更新后依然在**。
|
|
251
|
+
- **控制台会为它自动补齐**(否则常见结果是"装好了却加载不起来"):
|
|
252
|
+
- **目标目录自己的 `dependencies`**:在这个目录里执行一次安装,把本地目录缺的依赖补上;
|
|
253
|
+
- **框架 peer 垫片根**:本地目录位于 profile 树**之外**时,Node 的解析链走不到 profile 的
|
|
254
|
+
`node_modules`。控制台会为它**声明过、且当前解析不到**的框架 peer(`@deepseek-ai/*`)
|
|
255
|
+
在**插件根**建链接,指向 `~/.dsh/profiles/node_modules/@deepseek-ai/<包>`。
|
|
256
|
+
是**链接**而非复制 —— 两侧 realpath 相同,Cordis 服务类 / LLM 错误类的**模块身份与宿主共享**。
|
|
257
|
+
- **边界**(都会如实报告,不会假装成功):
|
|
258
|
+
- 本地目录被**移动或删除** ⇒ 链接即断(清单里的 `link:` 指向已不存在的位置);
|
|
259
|
+
- 垫片**只补**它声明过、且当前确实缺失的 peer;框架侧本来就没有的包不会被凭空造出来;
|
|
260
|
+
- 目标位置上**已经有的任何东西**(含悬空链接)**绝不覆盖**,只跳过并说明;
|
|
261
|
+
- **卸载会清掉控制台造的垫片**(只清它自己造的那些;你手工放的链接一律不碰);
|
|
262
|
+
- 补齐失败(目标目录不可写、装依赖失败等)⇒ **安装照常完成**,结果里如实写"未就绪 + 缺什么",
|
|
263
|
+
并给出可直接执行的动作。
|
|
264
|
+
|
|
239
265
|
### 静态索引市场(插件 / 技能双 tab)
|
|
240
266
|
|
|
241
267
|
> **混合架构**:浏览走静态索引(秒开、零 API 调用),搜索走实时通道(GitHub 搜索 API / 多源并行)——
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// L1 · domain —— install-finalize.js(安装收口结果 → 作业字段的**唯一映射**;2026-10-06 从 install-job.js 搬出)
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独成模块:install-job.js 贴着 600 行架构棘轮(tests/test-architecture-guard.mjs),而这块
|
|
4
|
+
// 本来就是自洽的一块 —— 收口(domain/persist.js)返回什么,作业(`installJobView` 下发给面板)就怎么显示,
|
|
5
|
+
// 中间**不许**再各判一套。搬移只搬移未改逻辑;0.5.41 只多一条:**真装失败 ⇒ 一键真装动作优先**
|
|
6
|
+
// (link 回落让插件能加载,但用户要的那一步"依赖交给 pnpm"没成,必须能一键补上;三处齐备时也要下发)。
|
|
7
|
+
//
|
|
8
|
+
// `deps` 是**离线测试缝**(与 marketProbes/sourceDeps 同一风格):正控/负控必须能确定性地让 lock 那一步
|
|
9
|
+
// 失败("故意让 lock 写入失败 ⇒ 必须报未持久化而不是成功"),生产调用方不传时形状与默认值一个字都不变。
|
|
10
|
+
|
|
11
|
+
import { ensurePersisted } from './persist.js'
|
|
12
|
+
import { sourcePersistOptions } from './source-spec.js'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 收口 + 落到作业字段。返回 `ensurePersisted` 的完整结果(调用方按需再读)。
|
|
16
|
+
* 只写这些字段(与旧代码逐字一致):`job.depNote` / `job.declared` / `job.entryId` / `job.persisted` /
|
|
17
|
+
* `job.persist` / `job.persistNote` / `job.suggestedAction`。
|
|
18
|
+
*/
|
|
19
|
+
async function finalizeInstallPersistence({
|
|
20
|
+
job, profileDir, patchPath, packageName, mode, taken = null, entries = null, registries = [],
|
|
21
|
+
source = null, deps = {},
|
|
22
|
+
}) {
|
|
23
|
+
const persist = await ensurePersisted({
|
|
24
|
+
profileDir, patchPath, packageName, mode, taken, entries, registries, deps,
|
|
25
|
+
...sourcePersistOptions(source),
|
|
26
|
+
})
|
|
27
|
+
const persistNotes = [persist.declared?.depNote, persist.declared?.lockNote, persist.lock?.lockNote, persist.lock?.depNote, ...persist.notes]
|
|
28
|
+
.filter((n) => typeof n === 'string' && n !== '')
|
|
29
|
+
if (persistNotes.length > 0) job.depNote = job.depNote === undefined ? persistNotes.join(';') : `${job.depNote};${persistNotes.join(';')}`
|
|
30
|
+
if (persist.declared?.changed === true) job.declared = persist.declared.spec
|
|
31
|
+
// entryId 的语义**与旧行为一致**:只有"用户补丁里的 insert 行"才算本作业注册的行(bundle 层提供的行
|
|
32
|
+
// 与运行时已有行都不算,旧代码在这两条路上也不设 entryId)——撤销安装的归属判定依赖这一点。
|
|
33
|
+
job.entryId = persist.after.parts.mount.via === 'patch-row' ? (persist.rowId ?? null) : null
|
|
34
|
+
job.persisted = persist.persisted
|
|
35
|
+
job.persist = { missing: persist.missing, actual: persist.after.actual, note: persist.note }
|
|
36
|
+
if (persist.persisted !== true) job.persistNote = persist.note
|
|
37
|
+
const persistSuggested = persist.suggested ?? null
|
|
38
|
+
if (persistSuggested !== null
|
|
39
|
+
&& (persistSuggested.kind === 'real-install' || job.suggestedAction === undefined || job.suggestedAction === null)) {
|
|
40
|
+
job.suggestedAction = persistSuggested
|
|
41
|
+
}
|
|
42
|
+
return persist
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { finalizeInstallPersistence }
|
|
@@ -18,7 +18,7 @@ import { tryPresetSourceChannel } from './preset-source.js'
|
|
|
18
18
|
import { settlePresetOutcome } from './preset-in-package.js'
|
|
19
19
|
import { alreadyInstalledTarget, verifyInstalledEntry } from './install-verify.js'
|
|
20
20
|
import { applyNpmNameHint, expandSubpackageCandidates, fetchRepoPackage, fetchRepoPackageEx, fetchSubpackageNames, npmNameHintForRepo, packageProbeErrorText, subpackageCandidates } from './market.js'
|
|
21
|
-
import {
|
|
21
|
+
import { finalizeInstallPersistence } from './install-finalize.js'
|
|
22
22
|
import { readPatchState, syncNameFromNote } from './patch.js'
|
|
23
23
|
import { listEntries } from './runtime.js'
|
|
24
24
|
import { detectSkillRepo, runSkillInstallJob } from './skills.js'
|
|
@@ -75,6 +75,10 @@ async function tryCandidateChannels({ job, ch, name, profileDir, registries, rep
|
|
|
75
75
|
const patchPath = join(profileDir, 'cordis.patch.yml')
|
|
76
76
|
let installedName = null
|
|
77
77
|
let lastError = null
|
|
78
|
+
// 0.5.41 接线:这次成功装上的**来源**(`{ spec, origin, kind }`)—— 取样通道(curl tarball /
|
|
79
|
+
// GitHub release 资产)知道真实下载地址,收口那一步据此走 `pnpm add <spec>` 真装(依赖交给 pnpm)。
|
|
80
|
+
// registry 通道不填(既有流程一字不改);git 通道也不填(它本来就是 pnpm 装的,清单里已是 pnpm 写的规格)。
|
|
81
|
+
let source = null
|
|
78
82
|
// 加法优化:包已在 node_modules 且名字匹配时,不再重复安装/触发 EPERM,直接进入启用流程
|
|
79
83
|
// (2026-09-27 搬进 install-verify.js#alreadyInstalledTarget:本文件贴着 600 行架构硬顶)
|
|
80
84
|
const already = alreadyInstalledTarget(profileDir, name, job)
|
|
@@ -100,6 +104,9 @@ async function tryCandidateChannels({ job, ch, name, profileDir, registries, rep
|
|
|
100
104
|
}
|
|
101
105
|
if (racedInfo.boxNote) job.curlNote += `(盒子验证:${racedInfo.boxNote})`
|
|
102
106
|
job.integrity = racedInfo.integrity ?? null // ④ 下载物摘要校验结果(加法;见 install-verify.js)
|
|
107
|
+
// 0.5.41:curl tarball 是**取样通道**(物是解压铺进去的,不是包管理器装的)——
|
|
108
|
+
// 真实来源规格(registry tarball URL)带回收口。registry 可解析时收口仍写版本号(不倒退)。
|
|
109
|
+
source = { spec: racedInfo.sourceSpec ?? null, origin: 'sampled', kind: 'registry-tarball' }
|
|
103
110
|
}
|
|
104
111
|
}
|
|
105
112
|
} catch (raceError) {
|
|
@@ -126,6 +133,8 @@ async function tryCandidateChannels({ job, ch, name, profileDir, registries, rep
|
|
|
126
133
|
job.curlNote = `已通过 curl 通道安装 v${info.version}${stillMissing.length > 0 ? `(依赖仍未补齐:${stillMissing.join('、')},网络恢复后建议重新安装)` : '(捆绑依赖已补齐)'}`
|
|
127
134
|
if (info.boxNote) job.curlNote += `(盒子验证:${info.boxNote})`
|
|
128
135
|
job.integrity = info.integrity ?? null // ④ 下载物摘要校验结果(加法;见 install-verify.js)
|
|
136
|
+
// 0.5.41:同上(取样通道 ⇒ 带真实 tarball URL 回去真装)
|
|
137
|
+
source = { spec: info.sourceSpec ?? null, origin: 'sampled', kind: 'registry-tarball' }
|
|
129
138
|
} catch (curlError) {
|
|
130
139
|
lastError = curlError
|
|
131
140
|
}
|
|
@@ -144,6 +153,9 @@ async function tryCandidateChannels({ job, ch, name, profileDir, registries, rep
|
|
|
144
153
|
const from = typeof info.sourceNote === 'string' && info.sourceNote !== '' ? `(来源:${info.sourceNote})` : ''
|
|
145
154
|
job.curlNote = `已通过 GitHub release 通道安装 v${info.version ?? '?'}${from}${stillMissing.length > 0 ? `(依赖仍未补齐:${stillMissing.join('、')},网络恢复后建议重新安装)` : '(捆绑依赖已补齐)'}`
|
|
146
155
|
if (info.boxNote) job.curlNote += `(盒子验证:${info.boxNote})`
|
|
156
|
+
// 0.5.41 接线(**本轮的核心**):GitHub release 资产(tarball URL)是取样通道 —— 把真实下载
|
|
157
|
+
// 地址带回给收口,由 `pnpm add <url>` 真装(依赖与 peer 交给包管理器,与官方 dsh plugin add 同效)。
|
|
158
|
+
source = { spec: info.sourceSpec ?? null, origin: 'sampled', kind: 'github-release' }
|
|
147
159
|
} catch (ghError) {
|
|
148
160
|
lastError = ghError
|
|
149
161
|
}
|
|
@@ -186,7 +198,7 @@ async function tryCandidateChannels({ job, ch, name, profileDir, registries, rep
|
|
|
186
198
|
installedName = retry.installedName ?? installedName
|
|
187
199
|
if (retry.error != null) lastError = retry.error
|
|
188
200
|
}
|
|
189
|
-
if (installedName !== null) return { installedName, lastError }
|
|
201
|
+
if (installedName !== null) return { installedName, lastError, source }
|
|
190
202
|
// Windows 原子替换失败(陈旧目录 / _tmp_ 残留)是 EPERM 类错误的根因:
|
|
191
203
|
// 清理后用主源重试一次(把 AI 人工修复经验自动化,减少 AI 兜底触发)
|
|
192
204
|
if (/EPERM|EACCES|rename/i.test(String(lastError?.message ?? ''))) {
|
|
@@ -356,6 +368,8 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
356
368
|
for (const id of [...patch.inserts, ...patch.disables, ...patch.forced]) taken.add(id)
|
|
357
369
|
let installedName = null
|
|
358
370
|
let lastError = null
|
|
371
|
+
// 0.5.41:成功后由哪个**来源**装上的(`{ spec, origin, kind } | null`)—— 一路传到收口那一步
|
|
372
|
+
let installedSource = null
|
|
359
373
|
let expanded = false
|
|
360
374
|
// 可配置软件源:主→备依次尝试(默认 npmmirror → npmjs,可增删自定义/内网源)
|
|
361
375
|
const registries = orderedRegistries(readSources())
|
|
@@ -401,7 +415,7 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
401
415
|
})
|
|
402
416
|
installedName = attempt.installedName
|
|
403
417
|
lastError = attempt.lastError
|
|
404
|
-
if (installedName !== null) break
|
|
418
|
+
if (installedName !== null) { installedSource = attempt.source ?? null; break }
|
|
405
419
|
}
|
|
406
420
|
// 预设收口(D-③ 源码装配成功 / D-⑥ 落地物里带预设):能按预设收口就直接 return —— 见 preset-in-package.js
|
|
407
421
|
// (这两条路都**没有 node_modules 插件**,写补丁行/声明依赖会让补丁行指向无入口模块 → 启动崩溃)。
|
|
@@ -430,7 +444,13 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
430
444
|
// 不能让用户面对"面板说失败、磁盘上却留了半个包和 _tmp_ 残留"的糊涂账。
|
|
431
445
|
const leftovers = cleanupAttemptedCandidates(profileDir, candidates)
|
|
432
446
|
job.leftovers = leftovers
|
|
447
|
+
// ★ 失败路径 B 的文案(0.5.41 加法):**只有如实失败 + 原因**。取样/下载本身失败时磁盘上
|
|
448
|
+
// 什么都没有 ⇒ 没有任何东西可挂 ⇒ 面板**绝不**出现"回落成功 / 已安装 / 已启用"之类说法。
|
|
449
|
+
// 这里把最后一次的真实失败原因接在既有文案之后(不改写、不掩盖),并保留既有诊断分类(可重试动作
|
|
450
|
+
// 由面板的「重试」按钮与 diagnosis.hint 承担)。
|
|
451
|
+
const reason = typeof lastError?.message === 'string' && lastError.message !== '' ? lastError.message : null
|
|
433
452
|
job.error = aiConsentFailureText(decision, leftovers, aiConsentTimeoutMs)
|
|
453
|
+
+ (reason === null ? '' : `;最后一次失败原因:${reason}`)
|
|
434
454
|
}
|
|
435
455
|
return
|
|
436
456
|
}
|
|
@@ -469,24 +489,28 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
469
489
|
} catch {}
|
|
470
490
|
// lock 对账(2026-09-21,用户报告的自更新缺陷同源):主包 + 本次被补装/对齐的聚合子包一起核对,
|
|
471
491
|
// 一次 pnpm add 把漂移的包全写进 lock;对不上就留 lockNote,让面板如实告知(不假装成功)。
|
|
492
|
+
// ★ 0.5.41:本次装上的包**有真实来源规格**时(取样通道 = curl/release 资产),这里的对账要**让开**:
|
|
493
|
+
// 它只会按"registry 查无此包"的老判据写成 link:(还顺手复制进 plugin-src、留一句"已按 link: 形式记录"),
|
|
494
|
+
// 而紧接着的收口会用真实来源规格(`pnpm add <spec>` 真装)把它声明对 —— 两处不再打架。
|
|
472
495
|
try {
|
|
473
496
|
// 同一个离线测试缝(见下方 `deps.persistDeps` 注释):安装路径上"动 lock"的两处都归它,
|
|
474
497
|
// 否则负控里这一处会先偷偷把 lock 写齐,收口那一步就再也测不出"未持久化"。
|
|
475
498
|
const reconcile = typeof deps.persistDeps?.reconcile === 'function' ? deps.persistDeps.reconcile : reconcileLockfile
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
499
|
+
const lockPackages = [installedName, ...syncedNames]
|
|
500
|
+
.filter((name) => !(installedSource !== null && typeof installedSource.spec === 'string' && installedSource.spec !== '' && name === installedName))
|
|
501
|
+
.map((name) => ({ name }))
|
|
502
|
+
if (lockPackages.length > 0) {
|
|
503
|
+
const lock = await reconcile({ profileDir, packages: lockPackages, registries })
|
|
504
|
+
job.lockUpdated = lock.lockUpdated
|
|
505
|
+
job.lockVersion = lock.lockVersion
|
|
506
|
+
job.lockMethod = lock.method
|
|
507
|
+
job.lockPackages = lock.packages
|
|
508
|
+
if (lock.lockNote !== null) job.lockNote = lock.lockNote
|
|
509
|
+
// 非常规来源(link:)写回说明:必须让用户看见(缺陷②最阴的地方就是"装完完全看不出问题")
|
|
510
|
+
if (lock.depNote !== null) job.depNote = lock.depNote
|
|
511
|
+
// 写回没生效时的**结构化**建议动作(加法;老客户端忽略该字段,向后兼容)
|
|
512
|
+
if (Array.isArray(lock.suggestedActions) && lock.suggestedActions.length > 0) job.suggestedAction = lock.suggestedActions[0]
|
|
513
|
+
}
|
|
490
514
|
} catch {}
|
|
491
515
|
job.stage = 'configuring'
|
|
492
516
|
// 2026-09-06 事故(i18n 更新变重复行):@linxin666/dsh-i18n 声明了 dsh.bundle.patch,
|
|
@@ -495,11 +519,11 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
495
519
|
const alreadyServed = listEntries(ports).some((e) => String(e.moduleName).startsWith(installedName)
|
|
496
520
|
|| String(e.moduleName) === installedName
|
|
497
521
|
|| packageNameOf(e.moduleName) === installedName)
|
|
498
|
-
// 安装收口(0.5.38
|
|
499
|
-
// ③ 挂载行(cordis.patch.yml 的 insert 行 / dsh.profile.bundles 的 bundle 层)。
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
522
|
+
// 安装收口(0.5.38 判据 / 0.5.41 接线):**一次动作内**把该插件钉住 —— ① 清单 spec
|
|
523
|
+
// ② pnpm-lock.yaml 条目 ③ 挂载行(cordis.patch.yml 的 insert 行 / dsh.profile.bundles 的 bundle 层)。
|
|
524
|
+
// 判据与写入的唯一承担者是 domain/persist.js;结果 → 作业字段的唯一映射在 domain/install-finalize.js
|
|
525
|
+
// (本轮把"本次装上的**真实来源规格**"一并带进去:取样通道 ⇒ `pnpm add <spec>` 真装;真装失败 ⇒
|
|
526
|
+
// 回落 link: 并如实报「已装上(link 方式)· 真装失败:… · 可一键真装」)。
|
|
503
527
|
let mode = 'insert'
|
|
504
528
|
if (await detectBundleOnly(profileDir, installedName)) {
|
|
505
529
|
mode = 'bundle'
|
|
@@ -526,23 +550,10 @@ async function runInstallJob(job, ports, deps = {}) {
|
|
|
526
550
|
mode = 'served'
|
|
527
551
|
job.note = `${installedName} 已由已安装条目提供,仅更新包本身,不重复注册`
|
|
528
552
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const persistNotes = [persist.declared?.depNote, persist.declared?.lockNote, persist.lock?.lockNote, persist.lock?.depNote, ...persist.notes]
|
|
534
|
-
.filter((n) => typeof n === 'string' && n !== '')
|
|
535
|
-
if (persistNotes.length > 0) job.depNote = job.depNote === undefined ? persistNotes.join(';') : `${job.depNote};${persistNotes.join(';')}`
|
|
536
|
-
if (persist.declared?.changed === true) job.declared = persist.declared.spec
|
|
537
|
-
// entryId 的语义**与旧行为一致**:只有"用户补丁里的 insert 行"才算本作业注册的行(bundle 层提供的行
|
|
538
|
-
// 与运行时已有行都不算,旧代码在这两条路上也不设 entryId)——撤销安装的归属判定依赖这一点。
|
|
539
|
-
job.entryId = persist.after.parts.mount.via === 'patch-row' ? (persist.rowId ?? null) : null
|
|
540
|
-
job.persisted = persist.persisted
|
|
541
|
-
job.persist = { missing: persist.missing, actual: persist.after.actual, note: persist.note }
|
|
542
|
-
if (persist.persisted !== true) {
|
|
543
|
-
job.persistNote = persist.note
|
|
544
|
-
if (persist.suggested !== null && persist.suggested !== undefined && (job.suggestedAction === undefined || job.suggestedAction === null)) job.suggestedAction = persist.suggested
|
|
545
|
-
}
|
|
553
|
+
await finalizeInstallPersistence({
|
|
554
|
+
job, profileDir, patchPath, packageName: installedName, mode, taken, entries: listEntries(ports), registries,
|
|
555
|
+
source: installedSource, deps: deps.persistDeps ?? {},
|
|
556
|
+
})
|
|
546
557
|
job.status = 'done'
|
|
547
558
|
} catch (error) {
|
|
548
559
|
job.status = 'failed'
|
|
@@ -181,7 +181,7 @@ async function curlManualInstall(profileDir, packageName, registries, signal = n
|
|
|
181
181
|
try {
|
|
182
182
|
writeFileSync(join(target, '.dsh-installed-at'), String(Date.now()), 'utf8')
|
|
183
183
|
} catch {}
|
|
184
|
-
return { version, missingDeps, boxNote: box.note, integrity }
|
|
184
|
+
return { version, missingDeps, boxNote: box.note, integrity, sourceSpec: tarball }
|
|
185
185
|
} finally {
|
|
186
186
|
rmSync(tmp, { recursive: true, force: true })
|
|
187
187
|
}
|
|
@@ -360,10 +360,14 @@ async function githubReleaseInstall(profileDir, repo, packageName, options = {})
|
|
|
360
360
|
const tgz = join(tmp, 'pkg.tgz')
|
|
361
361
|
let version = null
|
|
362
362
|
let sourceNote = null
|
|
363
|
+
// 0.5.41 接线:把**真实下载地址**带回给调用方(收口那一步据此走 `pnpm add <url>` 真装:
|
|
364
|
+
// 依赖与 peer 交给包管理器,而不是只挂一个目录 —— 见 domain/manifest.js#declareProfileDependency)。
|
|
365
|
+
let downloadUrl = null
|
|
363
366
|
if (plan.ok) {
|
|
364
367
|
const url = plan.asset?.browser_download_url
|
|
365
368
|
if (typeof url !== 'string' || url === '') throw new Error(`GitHub 通道:asset ${plan.file} 没有下载地址(接口未返回 browser_download_url)`)
|
|
366
369
|
await downloadReleaseArtifact(url, tgz, { bin })
|
|
370
|
+
downloadUrl = url
|
|
367
371
|
version = plan.version ?? null
|
|
368
372
|
sourceNote = `${plan.repo} 的 release ${plan.release?.tag_name ?? '(无 tag)'} 的资产 ${plan.file}`
|
|
369
373
|
} else if (plan.sourceFallback !== null) {
|
|
@@ -371,7 +375,8 @@ async function githubReleaseInstall(profileDir, repo, packageName, options = {})
|
|
|
371
375
|
const tag = plan.sourceFallback.tag
|
|
372
376
|
// codeload 官方源码 tarball(已验证本机可用 200)。GitHub 黑洞期由上层通道兜底
|
|
373
377
|
// (pnpm/git 通道),此处失败即报错保留旧版本。
|
|
374
|
-
|
|
378
|
+
downloadUrl = `https://codeload.github.com/${srcRepo}/tar.gz/refs/tags/${encodeURIComponent(tag)}`
|
|
379
|
+
await downloadReleaseArtifact(downloadUrl, tgz, { bin })
|
|
375
380
|
version = String(tag).replace(/^v/iu, '')
|
|
376
381
|
sourceNote = `${srcRepo} 的 release ${tag} 源码 tarball(该仓库没有任何与包名匹配的资产)`
|
|
377
382
|
} else {
|
|
@@ -409,7 +414,7 @@ async function githubReleaseInstall(profileDir, repo, packageName, options = {})
|
|
|
409
414
|
try {
|
|
410
415
|
writeFileSync(join(target, '.dsh-installed-at'), String(Date.now()), 'utf8')
|
|
411
416
|
} catch {}
|
|
412
|
-
return { version, missingDeps, boxNote: box.note, source: 'github', sourceNote }
|
|
417
|
+
return { version, missingDeps, boxNote: box.note, source: 'github', sourceNote, sourceSpec: downloadUrl }
|
|
413
418
|
} finally {
|
|
414
419
|
rmSync(tmp, { recursive: true, force: true })
|
|
415
420
|
}
|