@noob-stupid/dsh-plugin-console 0.5.40 → 0.5.42

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 CHANGED
@@ -303,9 +303,7 @@ Environment reminder: an accelerator/proxy that rewrites hosts makes the direct
303
303
 
304
304
  #### Development installs (`link:`)
305
305
 
306
- **Real install is the default.** Source-type packages (tarball URL / GitHub / local directory) hand
307
- their **real source spec** to the package manager (`pnpm add <spec>`) — dependencies and peers are
308
- guaranteed by pnpm, on the same channel as the official `dsh plugin add`.
306
+ Ordinary installs go through the package manager (`pnpm add`); that is the baseline, not a feature here.
309
307
 
310
308
  `link:` is the separate, **explicit** route: mount a **local directory / plugin you are developing**
311
309
  directly into the profile.
@@ -313,7 +311,7 @@ directly into the profile.
313
311
  - **Source edits take effect immediately**: `node_modules/<name>` in the profile is a **link** to your
314
312
  local directory, not a copy — edit the source, refresh the page. No reinstall, no repackaging.
315
313
  - **Who it is for**: plugin authors, or anyone who wants to edit a plugin's source. If you just want
316
- to *use* a plugin, the default real install is simpler.
314
+ to *use* a plugin, an ordinary install is simpler.
317
315
  - **What it pins**: the local artifact is written into the profile manifest `dependencies`
318
316
  (`link:<absolute path>`) and gets a matching entry in `pnpm-lock.yaml` — so lockfile health checks
319
317
  and rebuilds never treat it as an orphan, and it **survives restarts and instance updates**.
package/README.zh.md CHANGED
@@ -238,14 +238,13 @@ git clone https://github.com/Noob-stupid/dsh-plugin-gating-hub /tmp/dsh-plugin-c
238
238
 
239
239
  #### 开发式安装(`link:`)
240
240
 
241
- **默认是真装。** 来源型的包(tarball URL / GitHub / 本地目录)会把**真实来源规格**交给包管理器
242
- (`pnpm add <spec>`)——依赖与 peer 由 pnpm 保证,与官方 `dsh plugin add` 同一条通道。
241
+ 普通安装走包管理器(`pnpm add`)—— 那是基线,不是这里的卖点。
243
242
 
244
243
  `link:` 是**显式**的另一条路:把**本地目录 / 正在开发的插件**直接挂进 profile。
245
244
 
246
245
  - **改源码即时生效**:profile 里的 `node_modules/<包名>` 是一条指向本地目录的**链接**,不是副本 ——
247
246
  改完源码刷新页面即可,无需重装、无需重新打包。
248
- - **适合谁**:插件作者,或想直接改某个插件源码的人。只想"装个插件来用"的话,默认的真装更省心。
247
+ - **适合谁**:插件作者,或想直接改某个插件源码的人。只想"装个插件来用"的话,普通安装更省心。
249
248
  - **它钉住什么**:本地那份物会被写进 profile 清单的 `dependencies`(`link:<绝对路径>`),并在
250
249
  `pnpm-lock.yaml` 里留下对应条目 —— 所以「依赖锁体检 / 重建」不会把它当多余项清掉,
251
250
  **重启与实例更新后依然在**。
package/lib/client.js CHANGED
@@ -6,7 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  //#region styles
9
- const css = ".pc_section{width:100%;max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}.pc_section h3{margin:0;font-size:13px;font-weight:600;line-height:20px}.pc_message{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;margin:0}.pc_messageRow{display:flex;align-items:center;gap:10px}.pc_spinner{width:14px;height:14px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}@keyframes pcspin{to{transform:rotate(360deg)}}.pc_message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.pc_message[data-warn=true]{color:var(--dsw-alias-state-warning-primary)}.pc_list{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none;display:grid}.pc_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_rowTop{align-items:center;gap:8px;display:flex}.pc_name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.pc_tag{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px;flex:none}.pc_tag[data-enabled=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-user=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_tag[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_status[data-pending=true]{color:var(--dsw-alias-state-warning-primary)}.pc_toggle[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-skill=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-system=true]{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.pc_tag[data-disabled=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tag[data-suite=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-error=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tabGroup{display:inline-flex;gap:4px;align-items:center}.pc_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;gap:6px;display:flex;align-items:center}.pc_phase[data-phase=failed]{color:var(--dsw-alias-state-error-primary)}.pc_phase[data-phase=active]{color:var(--dsw-alias-state-success-primary)}.pc_toggle{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_toggle:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_toggle:disabled{opacity:.5;cursor:default}.pc_search{gap:8px;display:flex}.pc_search input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;flex:1;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 12px;font-size:13px}.pc_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.pc_search button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:0 14px;font-size:13px}.pc_search button:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_market{flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;display:flex}.pc_item{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_itemTop{align-items:center;gap:8px;display:flex}.pc_itemTop a{color:var(--dsw-alias-state-business-primary);font-size:13px;font-weight:600;text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pc_stars{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}.pc_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_item button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;align-self:flex-start;font-size:12px}.pc_item button:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_item button:disabled{opacity:.5;cursor:default}.pc_detail{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}.pc_note[data-truncated=true]{color:var(--dsw-alias-state-warning-primary)}.pc_marketHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.pc_descWrap{flex-direction:column;display:flex}.pc_installedHead{display:flex;align-items:center;gap:10px}.pc_backTopWrap{position:fixed;top:56px;right:20px;z-index:100;display:flex;flex-direction:column;gap:8px}.pc_backTop{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(22,27,34,.72);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);cursor:pointer;backdrop-filter:blur(4px);opacity:.7}.pc_backTop:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_descTopbar{display:flex;justify-content:flex-end;margin:2px 0}.pc_ghpill{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 10px;font-size:11px;line-height:18px}.pc_ghpill[data-on=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_extraBtn{opacity:.75;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 10px;font-size:12px;line-height:16px;flex:none;transition:opacity .15s ease,border-color 1.5s ease,color 1.5s ease}.pc_extraBtn:hover{opacity:1}.pc_extraBtn[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_extraBtn[data-flash=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_view{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:2px 10px;font-size:12px;line-height:18px;flex:none}.pc_floatPanel{position:fixed;left:20px;top:50%;transform:translateY(-50%);width:340px;max-height:75vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:50;flex-direction:column;gap:8px;display:flex}.pc_refresh{position:sticky;bottom:14px;align-self:flex-end;margin-top:8px;z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_refresh:hover{opacity:.9}.pc_date{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;flex:none}.pc_searchInner{position:relative;flex:1;display:flex;align-items:center;min-width:0}.pc_searchInner input{padding-right:32px;width:100%}.pc_starBtn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;opacity:.75;transition:opacity .15s ease;flex:none}.pc_starBtn:hover{opacity:1}.pc_starBtn[data-active=true]{opacity:1;color:var(--dsw-alias-state-business-primary)}.pc_restartBtn{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_restartBtn:hover{opacity:.9}.pc_relaunchFloat{position:fixed;left:20px;top:50%;transform:translateY(-50%);z-index:70;opacity:.85;border:1px solid var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_relaunchFloat:hover{opacity:1}.pc_aiToggle{position:fixed;right:20px;top:20px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiToggle:hover{opacity:1}.pc_aiToggle[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_sourcesFloat{position:fixed;right:20px;top:104px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_sourcesFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat{position:fixed;right:20px;top:204px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_modeFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_trashBtn{border:0;background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:16px;padding:2px 6px;flex:none;display:flex;align-items:center;border-radius:6px;transition:color .15s ease}.pc_trashBtn:hover{color:var(--dsw-alias-state-error-primary)}.pc_consentRemember{display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer}.pc_modalBackdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:center;justify-content:center}.pc_modalCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:16px 18px;width:min(760px,calc(100vw - 64px));max-height:calc(100vh - 96px);overflow-y:auto;overscroll-behavior:contain;flex-direction:column;gap:10px;display:flex;box-shadow:0 12px 32px rgba(0,0,0,.25)}.pc_modalCard::-webkit-scrollbar{width:8px}.pc_modalCard::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:4px}.pc_srcUrl{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%}.pc_accWrap{display:flex;flex-direction:column;gap:8px}.pc_acc{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}.pc_accHead{width:100%;text-align:left;background:transparent;border:0;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;font-weight:600;line-height:20px;padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:6px}.pc_accHead:hover{background:var(--dsw-alias-bg-layer-3)}.pc_accHead[data-open=true]{border-bottom:1px solid var(--dsw-alias-border-l2)}.pc_accBody{padding:10px 12px;display:flex;flex-direction:column;gap:8px}.pc_ghpill{background:0 0;cursor:pointer}.pc_ghwrap{position:relative;display:inline-flex}.pc_sourceMenu{position:absolute;right:0;top:calc(100% + 6px);z-index:90;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:4px;flex-direction:column;display:flex;min-width:120px;box-shadow:0 8px 24px rgba(0,0,0,.18)}.pc_sourceOpt{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 10px;text-align:left;font-size:12px;line-height:16px}.pc_sourceOpt[data-active=true]{color:var(--dsw-alias-state-business-primary);font-weight:600}.pc_sourceOpt:hover{background:var(--dsw-alias-bg-layer-3)}.pc_aiEmpower{position:fixed;right:20px;top:48px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiEmpower:hover{opacity:.9}.pc_aiEmpower[data-busy=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_aiEmpower[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_compCard{position:fixed;left:8px;top:20px;width:320px;max-height:70vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:45;flex-direction:column;gap:8px;display:flex}.pc_compCard .pc_rowTop{flex-wrap:wrap}.pc_compCard .pc_toggle{padding:3px 8px;font-size:11px}.pc_compCard[data-hidden=true]{display:none}.pc_compDrop{background:rgba(24,28,34,.92);backdrop-filter:blur(6px);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,.35);flex-direction:column;display:flex;gap:2px}.pc_compDropRow{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 8px;text-align:left;font-size:12px;line-height:16px;flex-direction:column;display:flex;gap:2px}.pc_compDropRow:hover{background:var(--dsw-alias-bg-layer-2)}.pc_compDropUrl{color:var(--dsw-alias-label-tertiary);font-size:11px;word-break:break-all}.pc_compToggle{position:fixed;left:336px;top:20px;z-index:46;opacity:.55;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:rgba(22,27,34,.72);font:inherit;cursor:pointer;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1;backdrop-filter:blur(4px);transition:opacity .15s ease}.pc_compToggle:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain{position:fixed;right:70px;top:max(88px,21vh);z-index:71;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:border-color .15s ease}.pc_toolbarMain:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain[data-active=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolbarGroup{position:fixed;right:70px;top:calc(max(88px,21vh) + 36px);z-index:70;flex-direction:column;gap:8px;display:flex;transform-origin:top right;animation:pcDrawerIn .3s cubic-bezier(.34,1.56,.64,1)}@media (max-width:1160px){.pc_toolbarMain{top:max(150px,30vh)}.pc_toolbarGroup{top:calc(max(150px,30vh) + 36px)}.pc_aiToggle{top:104px}.pc_aiEmpower{top:132px}.pc_sourcesFloat{top:188px}.pc_modeFloat{top:288px}}@keyframes pcDrawerIn{from{opacity:0;transform:translateX(28px) scale(.94)}to{opacity:1;transform:translateX(0) scale(1)}}.pc_toolItem{opacity:.85;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_toolItem:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_switch{width:34px;height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;flex:none;transition:background .15s ease,border-color .15s ease}.pc_switch:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_switch[data-on=true]{background:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.pc_switchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;transition:left .15s ease}.pc_switch[data-on=true] .pc_switchKnob{left:18px}.pc_aiSpinner{width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}.pc_aiSpinBtn{position:fixed;right:152px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px}.pc_aiSpinBtn:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_tag[data-bad=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_progress{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:0;font-variant-numeric:tabular-nums}.pc_consentCard{border:2px solid var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px 14px;flex-direction:column;gap:8px;display:flex;box-shadow:0 0 0 3px rgba(255,176,32,.10)}.pc_consentCountdown{margin-left:auto;flex:none;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--dsw-alias-state-warning-primary)}.pc_consentApprove{border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:0 0;font:inherit;font-weight:600;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_consentApprove:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}";
9
+ const css = ".pc_section{width:100%;max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}.pc_section h3{margin:0;font-size:13px;font-weight:600;line-height:20px}.pc_message{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;margin:0}.pc_messageRow{display:flex;align-items:center;gap:10px}.pc_spinner{width:14px;height:14px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}@keyframes pcspin{to{transform:rotate(360deg)}}.pc_message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.pc_message[data-warn=true]{color:var(--dsw-alias-state-warning-primary)}.pc_list{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none;display:grid}.pc_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_rowTop{align-items:center;gap:8px;display:flex}.pc_name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.pc_tag{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px;flex:none}.pc_tag[data-enabled=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-user=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_tag[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_status[data-pending=true]{color:var(--dsw-alias-state-warning-primary)}.pc_toggle[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-skill=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-system=true]{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.pc_tag[data-disabled=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tag[data-suite=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-error=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tabGroup{display:inline-flex;gap:4px;align-items:center}.pc_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;gap:6px;display:flex;align-items:center}.pc_phase[data-phase=failed]{color:var(--dsw-alias-state-error-primary)}.pc_phase[data-phase=active]{color:var(--dsw-alias-state-success-primary)}.pc_toggle{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_toggle:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_toggle:disabled{opacity:.5;cursor:default}.pc_search{gap:8px;display:flex}.pc_search input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;flex:1;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 12px;font-size:13px}.pc_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.pc_search button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:0 14px;font-size:13px}.pc_search button:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_market{flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;display:flex}.pc_item{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_itemTop{align-items:center;gap:8px;display:flex}.pc_itemTop a{color:var(--dsw-alias-state-business-primary);font-size:13px;font-weight:600;text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pc_stars{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}.pc_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_item button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;align-self:flex-start;font-size:12px}.pc_item button:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_item button:disabled{opacity:.5;cursor:default}.pc_detail{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}.pc_note[data-truncated=true]{color:var(--dsw-alias-state-warning-primary)}.pc_marketHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.pc_descWrap{flex-direction:column;display:flex}.pc_installedHead{display:flex;align-items:center;gap:10px}.pc_backTopWrap{position:fixed;top:56px;right:20px;z-index:100;display:flex;flex-direction:column;gap:8px}.pc_backTop{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(22,27,34,.72);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);cursor:pointer;backdrop-filter:blur(4px);opacity:.7}.pc_backTop:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_descTopbar{display:flex;justify-content:flex-end;margin:2px 0}.pc_ghpill{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 10px;font-size:11px;line-height:18px}.pc_ghpill[data-on=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_extraBtn{opacity:.75;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 10px;font-size:12px;line-height:16px;flex:none;transition:opacity .15s ease,border-color 1.5s ease,color 1.5s ease}.pc_extraBtn:hover{opacity:1}.pc_extraBtn[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_extraBtn[data-flash=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_view{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:2px 10px;font-size:12px;line-height:18px;flex:none}.pc_floatPanel{position:fixed;left:20px;top:50%;transform:translateY(-50%);width:340px;max-height:75vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:50;flex-direction:column;gap:8px;display:flex}.pc_refresh{position:sticky;bottom:14px;align-self:flex-end;margin-top:8px;z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_refresh:hover{opacity:.9}.pc_date{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;flex:none}.pc_searchInner{position:relative;flex:1;display:flex;align-items:center;min-width:0}.pc_searchInner input{padding-right:32px;width:100%}.pc_starBtn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;opacity:.75;transition:opacity .15s ease;flex:none}.pc_starBtn:hover{opacity:1}.pc_starBtn[data-active=true]{opacity:1;color:var(--dsw-alias-state-business-primary)}.pc_restartBtn{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_restartBtn:hover{opacity:.9}.pc_relaunchFloat{position:fixed;left:20px;top:50%;transform:translateY(-50%);z-index:70;opacity:.85;border:1px solid var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_relaunchFloat:hover{opacity:1}.pc_aiToggle{position:fixed;right:20px;top:20px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiToggle:hover{opacity:1}.pc_aiToggle[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_sourcesFloat{position:fixed;right:20px;top:104px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_sourcesFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat{position:fixed;right:20px;top:204px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_modeFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_trashBtn{border:0;background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:16px;padding:2px 6px;flex:none;display:flex;align-items:center;border-radius:6px;transition:color .15s ease}.pc_trashBtn:hover{color:var(--dsw-alias-state-error-primary)}.pc_consentRemember{display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer}.pc_modalBackdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:center;justify-content:center}.pc_modalCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:16px 18px;width:min(760px,calc(100vw - 64px));max-height:calc(100vh - 96px);overflow-y:auto;overscroll-behavior:contain;flex-direction:column;gap:10px;display:flex;box-shadow:0 12px 32px rgba(0,0,0,.25)}.pc_modalCard::-webkit-scrollbar{width:8px}.pc_modalCard::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:4px}.pc_srcUrl{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%}.pc_accWrap{display:flex;flex-direction:column;gap:8px}.pc_acc{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}.pc_accHead{width:100%;text-align:left;background:transparent;border:0;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;font-weight:600;line-height:20px;padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:6px}.pc_accHead:hover{background:var(--dsw-alias-bg-layer-3)}.pc_accHead[data-open=true]{border-bottom:1px solid var(--dsw-alias-border-l2)}.pc_accBody{padding:10px 12px;display:flex;flex-direction:column;gap:8px}.pc_ghpill{background:0 0;cursor:pointer}.pc_ghwrap{position:relative;display:inline-flex}.pc_sourceMenu{position:absolute;right:0;top:calc(100% + 6px);z-index:90;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:4px;flex-direction:column;display:flex;min-width:120px;box-shadow:0 8px 24px rgba(0,0,0,.18)}.pc_sourceOpt{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 10px;text-align:left;font-size:12px;line-height:16px}.pc_sourceOpt[data-active=true]{color:var(--dsw-alias-state-business-primary);font-weight:600}.pc_sourceOpt:hover{background:var(--dsw-alias-bg-layer-3)}.pc_aiEmpower{position:fixed;right:20px;top:48px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiEmpower:hover{opacity:.9}.pc_aiEmpower[data-busy=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_aiEmpower[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_compCard{position:fixed;left:8px;top:20px;width:320px;max-height:70vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:45;flex-direction:column;gap:8px;display:flex}.pc_compCard .pc_rowTop{flex-wrap:wrap}.pc_compCard .pc_toggle{padding:3px 8px;font-size:11px}.pc_compCard[data-hidden=true]{display:none}.pc_compDrop{background:rgba(24,28,34,.92);backdrop-filter:blur(6px);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,.35);flex-direction:column;display:flex;gap:2px}.pc_compDropRow{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 8px;text-align:left;font-size:12px;line-height:16px;flex-direction:column;display:flex;gap:2px}.pc_compDropRow:hover{background:var(--dsw-alias-bg-layer-2)}.pc_compDropUrl{color:var(--dsw-alias-label-tertiary);font-size:11px;word-break:break-all}.pc_compToggle{position:fixed;left:336px;top:20px;z-index:46;opacity:.55;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:rgba(22,27,34,.72);font:inherit;cursor:pointer;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1;backdrop-filter:blur(4px);transition:opacity .15s ease}.pc_compToggle:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain{position:fixed;right:70px;top:max(88px,21vh);z-index:71;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:border-color .15s ease}.pc_toolbarMain:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain[data-active=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolbarGroup{position:fixed;right:70px;top:calc(max(88px,21vh) + 36px);z-index:70;flex-direction:column;gap:8px;display:flex;transform-origin:top right;animation:pcDrawerIn .3s cubic-bezier(.34,1.56,.64,1)}@media (max-width:1160px){.pc_toolbarMain{top:max(150px,30vh)}.pc_toolbarGroup{top:calc(max(150px,30vh) + 36px)}.pc_aiToggle{top:104px}.pc_aiEmpower{top:132px}.pc_sourcesFloat{top:188px}.pc_modeFloat{top:288px}}@keyframes pcDrawerIn{from{opacity:0;transform:translateX(28px) scale(.94)}to{opacity:1;transform:translateX(0) scale(1)}}.pc_toolItem{opacity:.85;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_toolItem:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_switch{width:34px;height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;flex:none;transition:background .15s ease,border-color .15s ease}.pc_switch:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_switch[data-on=true]{background:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.pc_switchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;transition:left .15s ease}.pc_switch[data-on=true] .pc_switchKnob{left:18px}.pc_aiSpinner{width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}.pc_aiSpinBtn{position:fixed;right:152px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px}.pc_aiSpinBtn:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_tag[data-bad=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_progress{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:0;font-variant-numeric:tabular-nums}.pc_consentCard{border:2px solid var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px 14px;flex-direction:column;gap:8px;display:flex;box-shadow:0 0 0 3px rgba(255,176,32,.10)}.pc_consentCountdown{margin-left:auto;flex:none;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--dsw-alias-state-warning-primary)}.pc_consentApprove{border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:0 0;font:inherit;font-weight:600;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_consentApprove:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_starFoot{margin-top:auto;position:sticky;bottom:0;justify-content:flex-end;display:flex;padding:6px 0 2px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 16px 0 var(--dsw-alias-bg-layer-1)}.pc_starLink{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:16px;text-decoration:none;display:inline-flex;flex:none;border-radius:4px}.pc_starLink svg{flex:none;display:block}.pc_starLink:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline;text-underline-offset:3px}.pc_starLink:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}";
10
10
  const tagId = "@noob-stupid/dsh-plugin-console/PluginConsoleTab.css";
11
11
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
12
12
  const tag = document.createElement("style");
@@ -70,6 +70,8 @@ window.__ModuleLoader__.load({
70
70
  progress: "pc_progress",
71
71
  modalBackdrop: "pc_modalBackdrop",
72
72
  modalCard: "pc_modalCard",
73
+ starFoot: "pc_starFoot",
74
+ starLink: "pc_starLink",
73
75
  ghwrap: "pc_ghwrap",
74
76
  sourceMenu: "pc_sourceMenu",
75
77
  sourceOpt: "pc_sourceOpt",
@@ -231,6 +233,8 @@ window.__ModuleLoader__.load({
231
233
  gateRowFromUpgrade: "来源:框架升级预扫",
232
234
  gateRowAt: " · {t}",
233
235
  gateRowAdoptable: " · 已通过源码扫描,可解锁",
236
+ starHint: "喜欢这个插件?去 GitHub 点个 Star",
237
+ starHintTitle: "在新标签页打开 GitHub 项目",
234
238
  fwFailedButUpgraded: "框架本体其实已经升到 {v} 并已生效——失败的只是脚本最后「重启服务」那一步。可点「重启服务」确认,或必要时回滚。",
235
239
  fwPanelBtn: "框架",
236
240
  fwPanelTitle: "框架升级 / 回滚",
@@ -796,6 +800,8 @@ window.__ModuleLoader__.load({
796
800
  gateRowFromUpgrade: "Source: framework upgrade pre-scan",
797
801
  gateRowAt: " · {t}",
798
802
  gateRowAdoptable: " · passed the source scan, can be unlocked",
803
+ starHint: "Enjoying this plugin? Star it on GitHub",
804
+ starHintTitle: "Open the GitHub project in a new tab",
799
805
  fwFailedButUpgraded: "The framework itself is already on {v} and active — only the final \"restart service\" step failed. Use Restart service to confirm, or roll back if needed.",
800
806
  fwPanelBtn: "Framework",
801
807
  fwPanelTitle: "Framework upgrade / rollback",
@@ -4705,7 +4711,24 @@ window.__ModuleLoader__.load({
4705
4711
  gateRows.adopted.slice(0, 8).map((r) => el("span", { key: "a-" + String(r.rowId), className: styles.status },
4706
4712
  "✓ " + String(r.name) + (r.version ? " v" + r.version : "") + (r.adoptedAt ? " · " + gateTime(r.adoptedAt) : ""))))
4707
4713
  : null)
4708
- : null)))
4714
+ : null),
4715
+ // 0.5.42 加法(用户点名):右侧贴底的 Star 提示 —— 只加这一行 UI,
4716
+ // 不动任何开关逻辑/路由/门控语义。它是卡片的最后一个子项(整行,不占左侧内容宽度),
4717
+ // margin-top:auto 让它在卡片有富余高度时贴底;position:sticky;bottom:0 让正文
4718
+ // 超长滚动时它仍钉在卡片下沿(.pc_starFoot 见 #region styles)。
4719
+ el("div", { className: styles.starFoot },
4720
+ el("a", {
4721
+ className: styles.starLink,
4722
+ href: "https://github.com/Noob-stupid/dsh-plugin-gating-hub",
4723
+ target: "_blank",
4724
+ rel: "noreferrer",
4725
+ title: t("starHintTitle"),
4726
+ },
4727
+ // 标准 GitHub 标记(GitHub Octicons 的正统几何,16×16 视图、内联、currentColor 随文字着色;
4728
+ // 尺寸 15px、次级灰、flex 居中对齐文字)
4729
+ el("svg", { width: 15, height: 15, viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", focusable: "false" },
4730
+ el("path", { d: "M6.766 11.328c-2.063-.25-3.516-1.734-3.516-3.656 0-.781.281-1.625.75-2.188-.203-.515-.172-1.609.063-2.062.625-.078 1.468.25 1.968.703.594-.187 1.219-.281 1.985-.281.765 0 1.39.094 1.953.265.484-.437 1.344-.765 1.969-.687.218.422.25 1.515.046 2.047.5.593.766 1.39.766 2.203 0 1.922-1.453 3.375-3.547 3.64.531.344.89 1.094.89 1.954v1.625c0 .468.391.734.86.547C13.781 14.359 16 11.53 16 8.03 16 3.61 12.406 0 7.984 0 3.563 0 0 3.61 0 8.031a7.88 7.88 0 0 0 5.172 7.422c.422.156.828-.125.828-.547v-1.25c-.219.094-.5.156-.75.156-1.031 0-1.64-.562-2.078-1.609-.172-.422-.36-.672-.719-.719-.187-.015-.25-.093-.25-.187 0-.188.313-.328.625-.328.453 0 .844.281 1.25.86.313.452.64.655 1.031.655s.641-.14 1-.5c.266-.265.47-.5.657-.656" })),
4731
+ el("span", null, t("starHint"))))))
4709
4732
  : null,
4710
4733
  aiOpen
4711
4734
  ? el("div", { className: styles.modalBackdrop },
@@ -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 { ensurePersisted } from './persist.js'
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 lock = await reconcile({
477
- profileDir,
478
- packages: [installedName, ...syncedNames].map((name) => ({ name })),
479
- registries,
480
- })
481
- job.lockUpdated = lock.lockUpdated
482
- job.lockVersion = lock.lockVersion
483
- job.lockMethod = lock.method
484
- job.lockPackages = lock.packages
485
- if (lock.lockNote !== null) job.lockNote = lock.lockNote
486
- // 非常规来源(link:)写回说明:必须让用户看见(缺陷②最阴的地方就是"装完完全看不出问题")
487
- if (lock.depNote !== null) job.depNote = lock.depNote
488
- // 写回没生效时的**结构化**建议动作(加法;老客户端忽略该字段,向后兼容)
489
- if (Array.isArray(lock.suggestedActions) && lock.suggestedActions.length > 0) job.suggestedAction = lock.suggestedActions[0]
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):**一次动作内**把该插件钉住 —— ① 清单 spec ② pnpm-lock.yaml 条目
499
- // ③ 挂载行(cordis.patch.yml 的 insert 行 / dsh.profile.bundles 的 bundle 层)。
500
- // 三处齐备才算 `persisted`;缺任何一处都**如实**报「未持久化」+ 一键钉住动作 —— 绝不报成功
501
- // (用户真机现场:dshmarket 装了、bundle 行挂着,清单里却没有条目 ⇒ 重启/任何 pnpm 操作即消失)。
502
- // 判据与写入的唯一承担者是 domain/persist.js(别处不许再各判一套)。
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
- // `deps.persistDeps` 是**离线测试缝**(与 marketProbes/sourceDeps 同一风格):正控/负控必须能
530
- // 确定性地让 lock 那一步失败("故意让 lock 写入失败 ⇒ 必须报未持久化而不是成功"),
531
- // 生产调用方不传第三个参数时形状与默认值一个字都不变。
532
- const persist = await ensurePersisted({ profileDir, patchPath, packageName: installedName, mode, taken, entries: listEntries(ports), registries, deps: deps.persistDeps ?? {} })
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
- await downloadReleaseArtifact(`https://codeload.github.com/${srcRepo}/tar.gz/refs/tags/${encodeURIComponent(tag)}`, tgz, { bin })
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
  }
@@ -12,8 +12,9 @@
12
12
 
13
13
  import { readFile, writeFile } from 'node:fs/promises'
14
14
  import { join } from 'node:path'
15
- import { linkSpecFor, materializePackageForLink, probeFailureKind, probeRegistryPackage, sameSourceSpec, suggestedPinAction } from './dep-source.js'
15
+ import { canonicalSourceSpec, linkSpecFor, materializePackageForLink, probeFailureKind, probeRegistryPackage, sameSourceSpec, suggestedPinAction } from './dep-source.js'
16
16
  import { ensureLinkSelfSufficiency, removeLinkSelfSufficiency } from './link-self-sufficiency.js'
17
+ import { composeSourceInstallNote, forgetSourceSpec, isLinkSourceSpec, isRealInstallSpec, recordSourceSpec, suggestedRealInstallAction } from './source-spec.js'
17
18
  import { queuedWrite } from '../infra/fsx.js'
18
19
  import { runPnpmAdd } from '../infra/exec.js'
19
20
  import { resolvePackageJson } from '../infra/paths.js'
@@ -183,21 +184,58 @@ async function planDependencySpec({ profileDir, packageName, version, registries
183
184
  * lockSynced: boolean, lockNote: string|null, depNote: string|null, suggested: object|null }}
184
185
  */
185
186
  async function declareProfileDependency(profileDir, packageName, version = null, options = {}) {
186
- const { syncLock = true, registries = [], probe = null, home = undefined, profile = null, sourceSpec = null, installSource = null } = options
187
+ const { syncLock = true, registries = [], probe = null, home = undefined, profile = null, sourceSpec = null, installSource = null, sourceOrigin = null, sourceKind = null } = options
187
188
  const empty = { changed: false, version: null, spec: null, form: null, reason: null, lockSynced: false, lockNote: null, depNote: null, suggested: null, sourceInstall: null, selfSufficiency: null }
188
189
  const realSpec = typeof sourceSpec === 'string' && sourceSpec.trim() !== '' ? sourceSpec.trim() : null
190
+ const homeOpt = home === undefined ? {} : { home }
191
+ // `link:` 规格 = **显式开发式安装**(本地目录的正常路径):**不**交给 pnpm 真装,
192
+ // 直接走既有 link: 路径 + 自足补齐(判据 C:本地目录不涉及下载,link: 就是它的正常形态)。
193
+ const linkSource = realSpec !== null && isLinkSourceSpec(realSpec)
194
+ const installSpec = realSpec !== null && linkSource !== true && isRealInstallSpec(realSpec) ? realSpec : null
189
195
  // ★ 来源型默认【真装】(2026-10-06 加法):给了真实来源规格、而磁盘上还没有这个包时,
190
196
  // **先真装**(pnpm add <spec>,依赖/peer 交给包管理器);装失败也**不拒绝** ——
191
197
  // 如实记 sourceInstall 后继续走既有判定(退回 link: 并由自足性补齐)。
192
198
  let sourceInstall = null
193
- if (realSpec !== null && await installedVersionOf(profileDir, packageName) === null) {
194
- sourceInstall = await installSourceDependency({ profileDir, spec: realSpec, registries, install: installSource })
199
+ if (installSpec !== null && await installedVersionOf(profileDir, packageName) === null) {
200
+ sourceInstall = await installSourceDependency({ profileDir, spec: installSpec, registries, install: installSource })
201
+ recordSourceSpec({ packageName, spec: installSpec, kind: sourceKind, install: sourceInstall, ...homeOpt })
195
202
  }
196
203
  const resolved = typeof version === 'string' && version !== '' ? version : await installedVersionOf(profileDir, packageName)
197
- if (resolved === null) return { ...empty, sourceInstall, reason: '读不到已安装版本' }
204
+ if (resolved === null) {
205
+ // ★ 失败路径 B(取样/下载本身失败 ⇒ 物不在盘上):只有如实失败 + 原因,**绝不**出现
206
+ // "已装上 / 回落成功 / 已安装"之类说法(判据:回落只对"物已在盘上"成立)。
207
+ const reason = sourceInstall !== null && sourceInstall.ok !== true
208
+ ? `真装失败:${sourceInstall.error};磁盘上没有任何副本 —— 无可挂载(清单未改动)`
209
+ : '读不到已安装版本'
210
+ return { ...empty, sourceInstall, reason, depNote: composeSourceInstallNote({ packageName, install: sourceInstall }) ?? reason }
211
+ }
198
212
  // 探测/物化放在写锁**之外**(网络探测最长 8 秒,占着清单写锁会堵住其它写)
199
- const plan = await planDependencySpec({ profileDir, packageName, version: resolved, registries, probe, home, profile, sourceSpec: realSpec })
200
- if (plan.spec === null) return { ...empty, version: resolved, form: plan.form, reason: plan.note, depNote: plan.note, suggested: plan.suggestion }
213
+ let plan = linkSource
214
+ ? {
215
+ form: 'link',
216
+ spec: canonicalSourceSpec(realSpec),
217
+ note: `${packageName}:按**显式开发式安装**记录(${realSpec})—— 不经 npm registry 解析,依赖与框架 peer 由 link 自足性补齐`,
218
+ suggestion: null,
219
+ }
220
+ : await planDependencySpec({ profileDir, packageName, version: resolved, registries, probe, home, profile, sourceSpec: installSpec })
221
+ // ★ 本轮接线(2026-10-06):盘上那份是**取样通道**铺的(curl / GitHub release 资产 / 源码装配),
222
+ // 而不是包管理器装的 —— 而 registry 又查无此包(plan 判成来源型)⇒ **补一次真装**,
223
+ // 把依赖交给 pnpm(与官方 dsh plugin add 同一条通道)。
224
+ // 真装失败 ⇒ 回落既有 `link:` 路径:**回落只对"物已在盘上"成立**,这里正是(resolved 非空)。
225
+ if (installSpec !== null && sourceInstall === null && sourceOrigin === 'sampled' && plan.form === 'source') {
226
+ sourceInstall = await installSourceDependency({ profileDir, spec: installSpec, registries, install: installSource })
227
+ recordSourceSpec({ packageName, spec: installSpec, kind: sourceKind, install: sourceInstall, ...homeOpt })
228
+ if (sourceInstall.ok !== true) {
229
+ plan = await planDependencySpec({ profileDir, packageName, version: resolved, registries, probe, home, profile, sourceSpec: null })
230
+ }
231
+ }
232
+ const installNote = composeSourceInstallNote({ packageName, install: sourceInstall, plan })
233
+ if (plan.spec === null) {
234
+ return {
235
+ ...empty, version: resolved, form: plan.form, reason: plan.note, sourceInstall,
236
+ depNote: joinNotes(installNote, plan.note), suggested: plan.suggestion,
237
+ }
238
+ }
201
239
  const result = await queuedWrite(async () => {
202
240
  const manifest = await readManifest(profileDir)
203
241
  const deps = { ...(manifest.dependencies ?? {}) }
@@ -215,17 +253,33 @@ async function declareProfileDependency(profileDir, packageName, version = null,
215
253
  const selfSufficiency = plan.form === 'link'
216
254
  ? await ensureLinkSelfSufficiency({ profileDir, packageName, spec: plan.spec, home })
217
255
  : null
218
- const out = { ...empty, changed: result.changed, version: resolved, spec: plan.spec, form: plan.form, depNote: plan.note, suggested: plan.suggestion, sourceInstall, selfSufficiency }
256
+ // 真装失败 ⇒ 结构化「一键真装」动作优先(此时"钉住/自足"类动作都不是用户想要的那一步);
257
+ // 它的 payload 里**没有** spec —— 规格由服务端从记录里读(见 domain/source-spec.js)。
258
+ const installAction = sourceInstall !== null && sourceInstall.ok !== true
259
+ ? suggestedRealInstallAction({ packageName, spec: sourceInstall.spec, profile, profileDir, reason: sourceInstall.error })
260
+ : null
261
+ const out = {
262
+ ...empty, changed: result.changed, version: resolved, spec: plan.spec, form: plan.form,
263
+ depNote: joinNotes(installNote, plan.note),
264
+ // 优先级:真装失败的「一键真装」> 自足性动作 > 计划里的建议(与 syncLock:false 的旧行为逐字一致)
265
+ suggested: installAction ?? (selfSufficiency === null ? null : selfSufficiency.action) ?? plan.suggestion,
266
+ sourceInstall, selfSufficiency,
267
+ }
219
268
  if (syncLock !== true) return out
220
269
  // **写完清单必须把 lock 拉齐**(2026-09-24 CI 实测):只写清单不写 lock → 清单/lock 不一致 →
221
270
  // 之后任何 pnpm 操作都会硬失败(ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS)。
222
271
  // 放在写队列**之外**执行(对账本身会跑 pnpm,不该占着清单写锁);失败只记 note,不让安装失败。
223
- const synced = await syncLockFor(profileDir, [packageName], { registries, home, profile })
272
+ const synced = await syncLockFor(profileDir, [packageName], {
273
+ registries, home, profile,
274
+ // 0.5.41:刚真装出来的 **tarball URL** 是来源事实 ⇒ 告诉对账"别把它规整回 link:"
275
+ // (否则收口最后一步会把自己刚写下的真实来源规格改掉,用户看到的是"真装了但清单里是 link:")。
276
+ keepUrlSpecs: plan.form === 'source' && /^https?:/u.test(String(plan.spec ?? '')) ? [packageName] : [],
277
+ })
224
278
  return {
225
279
  ...out,
226
280
  ...synced,
227
- depNote: joinNotes(plan.note, selfSufficiency === null ? null : selfSufficiency.note, synced.depNote),
228
- suggested: synced.suggested ?? (selfSufficiency === null ? null : selfSufficiency.action) ?? plan.suggestion,
281
+ depNote: joinNotes(installNote, plan.note, selfSufficiency === null ? null : selfSufficiency.note, synced.depNote),
282
+ suggested: installAction ?? synced.suggested ?? (selfSufficiency === null ? null : selfSufficiency.action) ?? plan.suggestion,
229
283
  }
230
284
  }
231
285
 
@@ -258,6 +312,7 @@ async function syncLockFor(profileDir, names, options = {}) {
258
312
  profileDir,
259
313
  packages: names.map((name) => ({ name })),
260
314
  registries: Array.isArray(options.registries) ? options.registries : [],
315
+ ...(Array.isArray(options.keepUrlSpecs) && options.keepUrlSpecs.length > 0 ? { keepUrlSpecs: options.keepUrlSpecs } : {}),
261
316
  ...(options.home === undefined ? {} : { home: options.home }),
262
317
  })
263
318
  return {
@@ -339,7 +394,9 @@ async function undeclareProfileDependency(profileDir, packageName, options = {})
339
394
  // 2026-10-06 加法:link 自足性造的垫片是我们加的,卸载时只清**我们造的**那些
340
395
  // (记录里逐个核对"现在仍是指向当初目标的链接",被人改过的一律不碰)。清不掉只如实报。
341
396
  const shims = removeLinkSelfSufficiency({ packageName, home: options.home ?? null })
342
- return { ...result, shimsRemoved: shims.removed, shimsKept: shims.kept, selfSufficiencyNote: shims.note }
397
+ // 2026-10-06 接线:来源规格记录同样"卸载即撤销" —— 否则下一次安装会拿旧规格去真装(来源早已变了)。
398
+ const sourceRecord = forgetSourceSpec({ packageName, home: options.home ?? null })
399
+ return { ...result, shimsRemoved: shims.removed, shimsKept: shims.kept, selfSufficiencyNote: shims.note, sourceSpecForgotten: sourceRecord.removed || sourceRecord.emptied }
343
400
  }
344
401
 
345
402
  /**
@@ -290,10 +290,16 @@ function suggestedPersistAction({ packageName, profile = null, profileDir = null
290
290
  * · ② 缺/形态不一致 → `reconcileLockfile`(既有对账通道,argv/写回形态同安装路径);
291
291
  * · ③ 缺 → `mode==='bundle'` 时补 `dsh.profile.bundles`;否则追加一条 insert 行(entry id 由
292
292
  * `deriveEntryId` 按既有规则产出)。`mode==='served'` 时**绝不写行**(已有行提供它,再写就是重复装配)。
293
+ *
294
+ * 2026-10-06 接线(0.5.41):`sourceSpec` / `sourceOrigin` 由**安装通道**一路传进来 ——
295
+ * 取样通道(curl / GitHub release 资产 / 源码装配)装的包,真实来源规格在这里被交给
296
+ * `declareProfileDependency({sourceSpec, sourceOrigin:'sampled'})` ⇒ `pnpm add <spec>` 真装,
297
+ * 依赖与 peer 由包管理器保证;真装失败则回落既有 `link:` 路径并**如实**报
298
+ * 「已装上(link 方式)· 真装失败:… · 可一键真装」(回落只对"物已在盘上"成立)。
293
299
  * 返回 `{ persisted, before, after, missing, declared, lock, notes, suggested, note, rowId }`;
294
300
  * 任何一步失败都**如实**留在 notes 里,绝不影响收口的返回值语义(persisted 只认最终读回结果)。
295
301
  */
296
- async function ensurePersisted({ profileDir, patchPath, packageName, mode = 'insert', taken = null, entries = null, registries = [], deps = {} } = {}) {
302
+ async function ensurePersisted({ profileDir, patchPath, packageName, mode = 'insert', taken = null, entries = null, registries = [], deps = {}, sourceSpec = null, sourceOrigin = null, sourceKind = null } = {}) {
297
303
  const pin = typeof deps.pin === 'function' ? deps.pin : pinProfileDependency
298
304
  const declare = typeof deps.declare === 'function' ? deps.declare : declareProfileDependency
299
305
  const setSpec = typeof deps.setSpec === 'function' ? deps.setSpec : setProfileDependency
@@ -322,7 +328,17 @@ async function ensurePersisted({ profileDir, patchPath, packageName, mode = 'ins
322
328
  declared = { changed: wrote.changed === true, spec: wrote.after ?? disk.spec, form: 'link', lockSynced: null, lockNote: null, depNote: null, suggested: null }
323
329
  notes.push(`清单已按磁盘真实值写成 ${wrote.after ?? disk.spec}(该链接指向用户自己的位置,未做物化复制)`)
324
330
  } else if (disk.form === 'version') {
325
- declared = await declare(profileDir, packageName, disk.version, { registries })
331
+ // 来源型(安装通道知道真实来源规格):把规格一并交给写入层 —— 由它决定"真装 / link:"
332
+ // (0.5.41 接线;没传规格时形状与默认值一个字都不变)。
333
+ // `deps.installSource` / `deps.syncLock` 是**离线测试缝**(与 deps.pin / deps.declare 同一风格):
334
+ // 正控/负控要能确定性地让"包管理器那一步"成功或失败、并让 lock 那一步不跑真 pnpm,
335
+ // 生产调用方不传时用的就是真实 `pnpm add` 与真实 lock 对账。
336
+ declared = await declare(profileDir, packageName, disk.version, {
337
+ registries,
338
+ ...(typeof sourceSpec === 'string' && sourceSpec !== '' ? { sourceSpec, sourceOrigin, sourceKind } : {}),
339
+ ...(typeof deps.installSource === 'function' ? { installSource: deps.installSource } : {}),
340
+ ...(deps.syncLock === false ? { syncLock: false } : {}),
341
+ })
326
342
  if (declared.changed !== true) notes.push(`清单里已有这个包(实际是 ${declared.spec ?? disk.version})`)
327
343
  if (declared.depNote !== null && declared.depNote !== undefined) notes.push(declared.depNote)
328
344
  } else {
@@ -336,7 +352,15 @@ async function ensurePersisted({ profileDir, patchPath, packageName, mode = 'ins
336
352
  const mid = persistReport({ profileDir, patchPath, packageName, entries })
337
353
  if (mid.parts.lock.ok !== true) {
338
354
  try {
339
- lock = await reconcile({ profileDir, packages: [{ name: packageName }], registries })
355
+ // 0.5.41:本次带来的是 **tarball URL** 来源规格(刚真装过)⇒ 不许对账把它规整回 link:
356
+ // (判据与 manifest.js#declareProfileDependency 的同一套:只有这一段来源型接线会点名 keepUrlSpecs)。
357
+ const keepUrlSpecs = (typeof sourceSpec === 'string' && /^https?:/u.test(sourceSpec.trim()))
358
+ || (declared !== null && declared !== undefined && declared.form === 'source' && /^https?:/u.test(String(declared.spec ?? '')))
359
+ ? [packageName] : []
360
+ lock = await reconcile({
361
+ profileDir, packages: [{ name: packageName }], registries,
362
+ ...(keepUrlSpecs.length > 0 ? { keepUrlSpecs } : {}),
363
+ })
340
364
  if (lock.lockNote !== null && lock.lockNote !== undefined) notes.push(lock.lockNote)
341
365
  else if (lock.depNote !== null && lock.depNote !== undefined) notes.push(lock.depNote)
342
366
  } catch (error) {
@@ -377,7 +401,15 @@ async function ensurePersisted({ profileDir, patchPath, packageName, mode = 'ins
377
401
  after.note = after.persisted ? null : persistNote(after)
378
402
  lockVerdict = { verified: lock.lockVerified, reason }
379
403
  }
380
- const suggested = after.persisted ? null : suggestedPersistAction({ packageName, profileDir, missing: after.missing, reason: after.note })
404
+ // 0.5.41 接线:真装失败时写入层会下发「一键真装」动作 —— 它优先于"钉住"(此时用户要的是真装那一步);
405
+ // 只有三处都没就位时仍按老规矩给「一键钉住」(收口更根本)。老客户端忽略该字段,向后兼容。
406
+ const declaredAction = declared !== null && declared !== undefined && declared.suggested !== null && declared.suggested !== undefined
407
+ ? declared.suggested
408
+ : null
409
+ const suggested = after.persisted
410
+ ? declaredAction
411
+ : suggestedPersistAction({ packageName, profileDir, missing: after.missing, reason: after.note })
412
+ const sourceInstall = declared !== null && declared !== undefined && declared.sourceInstall !== undefined ? declared.sourceInstall : null
381
413
  return {
382
414
  persisted: after.persisted,
383
415
  before,
@@ -392,6 +424,7 @@ async function ensurePersisted({ profileDir, patchPath, packageName, mode = 'ins
392
424
  notes,
393
425
  suggested,
394
426
  note: after.note,
427
+ sourceInstall,
395
428
  }
396
429
  }
397
430
 
@@ -37,18 +37,21 @@
37
37
  import { existsSync, readFileSync } from 'node:fs'
38
38
  import { join } from 'node:path'
39
39
  import { applyAllowBuilds, pendingApprovedBuilds, readWorkspaceApprovals } from './allow-builds.js'
40
- import { NPM_NAME_RE, suggestedAllowBuildsAction } from './dep-source.js'
40
+ import { NPM_NAME_RE, sameSourceSpec, suggestedAllowBuildsAction } from './dep-source.js'
41
41
  import { classifyInstallFailure, ignoredBuildsFrom } from './install-diagnose.js'
42
42
  import { runLockfileRepair } from './lockfile-health.js'
43
43
  import { pinProfileDependency, specOf } from './manifest.js'
44
44
  import { ensurePersisted } from './persist.js'
45
45
  import { lockVersion } from './selfupdate.js'
46
46
  import { overwritePreset } from './preset-source.js'
47
+ import { isRealInstallSpec, readSourceSpec, recordSourceSpec } from './source-spec.js'
47
48
  import { buildPnpmEnv, pnpmAddArgs, runPnpmWithFallback, unknownPnpmOption } from '../infra/exec.js'
48
49
 
49
50
  const DEFAULT_REGISTRY = 'https://registry.npmmirror.com'
50
- /** 动作白名单(客户端只能给这些 kind;执行分支与 argv 全在服务端)。 */
51
- const ACTION_KINDS = ['pin-dependency', 'reconcile-lock', 'allow-builds', 'overwrite-preset', 'persist-plugin']
51
+ /** 动作白名单(客户端只能给这些 kind;执行分支与 argv 全在服务端)。
52
+ * 0.5.41 登记(加法,不改既有动作):`real-install` —— 来源型安装的"出路":真装失败(回落 link:)后
53
+ * 一键把**记录里的真实来源规格**再交给包管理器(`pnpm add <spec>`)。规格只从服务端记录读。 */
54
+ const ACTION_KINDS = ['pin-dependency', 'reconcile-lock', 'allow-builds', 'overwrite-preset', 'persist-plugin', 'real-install']
52
55
  /** 一旦出现这些键就整条拒绝:本接口不存在"传命令"的位置。 */
53
56
  const COMMAND_KEYS = ['command', 'cmd', 'argv', 'args', 'exec', 'execute', 'shell', 'script', 'run', 'spawn', 'bin', 'shellCommand']
54
57
  const OUTPUT_LIMIT = 4000
@@ -73,7 +76,7 @@ function parseActionRequest(body) {
73
76
  return { ok: false, status: 400, error: `未知动作 ${action}(只允许 ${ACTION_KINDS.join(' / ')})` }
74
77
  }
75
78
  const packageName = typeof raw.packageName === 'string' ? raw.packageName.trim() : ''
76
- if (action === 'pin-dependency' || action === 'persist-plugin') {
79
+ if (action === 'pin-dependency' || action === 'persist-plugin' || action === 'real-install') {
77
80
  if (packageName === '') return { ok: false, status: 400, error: `${action} 需要 packageName` }
78
81
  if (packageName.length > 214 || !NPM_NAME_RE.test(packageName)) return { ok: false, status: 400, error: 'packageName 不是合法的 npm 包名' }
79
82
  }
@@ -307,6 +310,66 @@ async function runSuggestedAction({ body, profileDir, registries = [], deps = {}
307
310
  lock: { entry: null, synced: view?.action === 'repaired' || view?.action === 'noop' },
308
311
  }
309
312
  }
313
+ if (parsed.action === 'real-install') {
314
+ // ★ 0.5.41 加法(来源型"一键真装"):把**服务端记录里的真实来源规格**再交给包管理器装一次
315
+ // (`pnpm add <spec>`)—— 依赖与 peer 由 pnpm 保证,与官方 `dsh plugin add` 同一条通道。
316
+ // 为什么规格必须从记录读:动作接口只接受 `{ action, packageName, profile }`,客户端**没有位置**
317
+ // 传 spec 字符串(否则等于开了"任意来源/任意命令"的口子);记录由安装时写入(domain/source-spec.js)。
318
+ if (!isProfilePackage(profileDir, parsed.packageName)) {
319
+ return { ...base, ok: false, status: 400, error: `${parsed.packageName} 不在本 profile 的清单(dependencies / dsh.profile.bundles)里,拒绝改动(先安装)` }
320
+ }
321
+ const record = readSourceSpec({ packageName: parsed.packageName, home: deps.home ?? null })
322
+ if (record === null || isRealInstallSpec(record.spec) !== true) {
323
+ return {
324
+ ...base, ok: false, status: 400,
325
+ error: `没有 ${parsed.packageName} 的来源规格记录(或记录的规格不能真装):本动作只重放**这个实例装过的来源型包**的真实规格(tarball URL / git 族 / file:),registry 版本号与 link: 不在其列`,
326
+ }
327
+ }
328
+ const before = await specOf(profileDir, parsed.packageName)
329
+ const run = await runAddWithReleaseAgeRetry({ profileDir, spec: record.spec, registry, runAdd, timeoutMs: ACTION_ADD_TIMEOUT_MS })
330
+ const after = await specOf(profileDir, parsed.packageName)
331
+ const entry = lockVersion(profileDir, parsed.packageName)
332
+ const failure = run.ok === true ? null : classifyInstallFailure(`${run.stderr}\n${String(run.error?.message ?? '')}`)
333
+ const ignored = failure?.kind === 'ignored-builds'
334
+ const ignoredBuilds = ignored === true ? ignoredBuildsFrom(run.stderr) : []
335
+ // 真装结果记回记录(面板/报告据此看到"最近一次真装成没成")——写失败只影响记录,绝不影响本次结论。
336
+ recordSourceSpec({
337
+ packageName: parsed.packageName,
338
+ spec: record.spec,
339
+ kind: record.kind ?? null,
340
+ install: { ok: run.ok === true, error: run.ok === true ? null : String(run.error?.message ?? run.stderr ?? '').slice(0, 600) },
341
+ home: deps.home ?? null,
342
+ })
343
+ // 与 pin 分支同一口径(A-② 语义):清单里的 spec 已经是那条真实来源规格 ⇒ 目标状态已达成;
344
+ // ERR_PNPM_IGNORED_BUILDS 这类报错一个脚本都不执行、也不影响加载,不算"没装上"。
345
+ const reached = sameSourceSpec(profileDir, after, record.spec)
346
+ const ok = run.ok === true || (ignored === true && reached)
347
+ const notes = []
348
+ if (run.relaxedReleaseAge) notes.push('本机 pnpm 的 24h 供应链闸(minimumReleaseAge)拦下了这次真装,已**仅对这一条命令**追加 --config.minimumReleaseAge=0 重试一次(有安全代价,未写进任何配置文件)。')
349
+ if (ignored === true) notes.push(IGNORED_BUILDS_REACHED_NOTE)
350
+ if (run.ok !== true && reached) notes.push(`pnpm 那一步没跑完(exitCode=${run.exitCode}),但清单里的 spec 已经是这条真实来源规格(${after})—— 目标状态已达成,原始输出见 stderr。`)
351
+ return {
352
+ ...base,
353
+ kind: ignored === true ? 'ignored-builds' : 'real-install',
354
+ ignoredBuilds,
355
+ nextAction: ignored === true ? suggestedAllowBuildsAction({ packages: ignoredBuilds, profile: parsed.profile, reason: 'pnpm 因构建脚本未获批准而报错' }) : null,
356
+ ok,
357
+ partial: ok !== true && reached,
358
+ exitCode: run.exitCode,
359
+ command: `pnpm ${pnpmAddArgs(record.spec, registry).join(' ')}`,
360
+ stdout: run.stdout,
361
+ stderr: run.stderr,
362
+ relaxedReleaseAge: run.relaxedReleaseAge,
363
+ spec: record.spec,
364
+ manifest: { before, after, changed: before !== after, planned: record.spec, pinned: reached },
365
+ lock: { entry: entry ?? null, synced: typeof entry === 'string' && entry !== '' },
366
+ notes,
367
+ failureKind: failure?.kind ?? null,
368
+ reason: ok === true
369
+ ? `${parsed.packageName} 已按真实来源规格真装:${record.spec}(依赖与 peer 由包管理器保证)`
370
+ : `真装没跑成(exitCode=${run.exitCode}):${record.spec} —— 清单当前是 ${after ?? '(无该条目)'},原始输出见 stderr`,
371
+ }
372
+ }
310
373
  // pin-dependency
311
374
  if (!isProfilePackage(profileDir, parsed.packageName)) {
312
375
  return { ...base, ok: false, status: 400, error: `${parsed.packageName} 不在本 profile 的清单(dependencies / dsh.profile.bundles)里,拒绝改动` }
@@ -331,7 +331,7 @@ async function settleImporterEntries({ profileDir, items = [], deps = {}, regist
331
331
  * 查无此包(404)→ 物化到 `<DSH_HOME>/plugin-src/<包名>` 并写 `link:<绝对路径>`,同时回 depNote;
332
332
  * 连物化都做不到 → **跳过对齐且不碰 package.json**,只记 note。
333
333
  */
334
- async function reconcileLockfile({ profileDir, packageName = null, packages = null, registries = [], pnpmAdd = pnpmInstall, execOpts = {}, fetchJson = fetchJsonUrl, home = null, deps = {} } = {}) {
334
+ async function reconcileLockfile({ profileDir, packageName = null, packages = null, registries = [], pnpmAdd = pnpmInstall, execOpts = {}, fetchJson = fetchJsonUrl, home = null, deps = {}, keepUrlSpecs = [] } = {}) {
335
335
  const targets = Array.isArray(packages)
336
336
  ? packages.filter((p) => p && typeof p.name === 'string' && p.name !== '')
337
337
  : (packageName === null ? [] : [{ name: packageName, version: null }])
@@ -375,7 +375,14 @@ async function reconcileLockfile({ profileDir, packageName = null, packages = nu
375
375
  }
376
376
  return r.installed === r.lock || sourcePinned(r.lock)
377
377
  }
378
- const driftedOf = (rows) => rows.filter((r) => r.installed === null || !aligned(r) || misrecorded(r) || urlSpec(r))
378
+ // 0.5.41 加法:**刚按真实来源规格真装过**的包(tarball URL)**不算漂移** —— 那个 URL 是用户可见的
379
+ // 来源事实,lock 里的解析记录也正是 pnpm 自己在那次 `pnpm add <url>` 里写下的(带 integrity)。
380
+ // 这条口子只对调用方**点名**的包生效(调用方 = 刚刚真装它的那条路,见 manifest.js / persist.js),
381
+ // 其余任何 URL 形态照旧走下面的"URL → link:"规整(那条保护是给"pnpm 静默改写/来源不确定"的历史状态)。
382
+ const keepUrls = new Set((Array.isArray(keepUrlSpecs) ? keepUrlSpecs : []).filter((n) => typeof n === 'string' && n !== ''))
383
+ const driftedOf = (rows) => rows
384
+ .filter((r) => !(keepUrls.has(r.name) && typeof r.spec === 'string' && /^https?:/u.test(r.spec.trim())))
385
+ .filter((r) => r.installed === null || !aligned(r) || misrecorded(r) || urlSpec(r))
379
386
 
380
387
  const depNotes = []
381
388
  // 物化 + link: 计划(registry 查无此包时唯一安全的写回形式)。
@@ -0,0 +1,212 @@
1
+ // L1 · domain —— source-spec.js(**来源型安装的"真实来源规格"记录** + 一键真装动作;2026-10-06 接线)
2
+ //
3
+ // 为什么需要它(本轮的接线缺口):0.5.40 已经把能力做进了写入层 —— `declareProfileDependency({sourceSpec})`
4
+ // 会把**真实来源规格**写回清单,并由 `installSourceDependency` 走 `pnpm add <spec>` 真装(依赖与 peer
5
+ // 交给包管理器,与官方 `dsh plugin add` 同一条通道)。但**生产路径上没有一个调用方传过 sourceSpec**:
6
+ // curl / GitHub release 这些"取样通道"把包铺进 `node_modules` 之后,收口那一步只知道"磁盘上有个真实目录",
7
+ // 于是按老的 registry 404 判据写成 `link:`(复制到 plugin-src + 链接)——依赖还是没人管。
8
+ // 本轮把"这个包是从哪个来源装来的"从通道一路传到收口,并在两处留下可核对的东西:
9
+ // ① **记录**(本模块):`<DSH_HOME>/plugin-console/source-specs/<包名>.json` —— 记下真实来源规格与
10
+ // 最近一次真装结果。用途是让「一键真装」动作能**服务端自己**取到规格:动作接口只接受
11
+ // `{ action, packageName, profile }`,客户端**没有位置**传 spec 字符串(否则等于开了任意命令的口子)。
12
+ // ② **文案与动作**(本模块是唯一承担者):真装成功 / 真装失败的两种如实说法 + 结构化「一键真装」动作。
13
+ //
14
+ // 失败路径的语义(写死在这里,别处不许再解释一遍 —— 用户点名要求):
15
+ // A) 取样/下载**成功**、包管理器安装**失败** ⇒ 磁盘上确实有那份物化副本 ⇒ 允许回落既有 `link:`
16
+ // 路径(插件至少能加载)⇒ 文案必须含「已装上(link 方式)」+「真装失败:<原因>」+「可一键真装」。
17
+ // B) 取样/下载**本身失败** ⇒ 磁盘上什么都没有 ⇒ **没有任何东西可挂** ⇒ 只能如实失败
18
+ // (原因 + 重试);**严禁**出现"回落成功 / 已安装 / 已启用"之类说法。
19
+ // C) 本地目录:不涉及下载 ⇒ `link:` 是它的正常路径(+ 既有自足补齐)。
20
+ // 判据一句话:**回落只对"物已在盘上"成立;物不在盘上就只有如实失败**。
21
+ //
22
+ // 分层:L1 domain —— 不认识 cordis ctx;home 可注入(隔离测试用私有 DSH_HOME),一切失败都不抛。
23
+
24
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
25
+ import { dirname, join } from 'node:path'
26
+ import { dshHome } from '../infra/paths.js'
27
+ import { canonicalSourceSpec, profileNameOf } from './dep-source.js'
28
+
29
+ /** 记录目录(与 link-self-sufficiency 记录同族,都在 `<DSH_HOME>/plugin-console/` 下)。 */
30
+ const SOURCE_SPEC_DIR = 'source-specs'
31
+
32
+ /** `link:` = 显式开发式安装(本地目录的正常路径),**不**交给 pnpm 真装(见 C)。 */
33
+ const LINK_SPEC_RE = /^link:/u
34
+
35
+ /** 可以交给 `pnpm add <spec>` 的**来源型**规格(tarball URL / git 族 / file: / 别名协议)。
36
+ * 刻意**不含**版本号、版本范围、dist-tag:那些是 registry 路径,写回形态由既有判据决定(不倒退)。 */
37
+ const REAL_INSTALLABLE_RE = /^(?:https?:|git\+|git:|github:|gitlab:|bitbucket:|file:|workspace:|portal:|npm:|jsr:)/u
38
+
39
+ /** 记录目录 / 记录文件(一个包一份,便于一键真装与卸载清理精确定位)。 */
40
+ function sourceSpecRecordDir(home = null) {
41
+ return join(typeof home === 'string' && home !== '' ? home : dshHome(), 'plugin-console', SOURCE_SPEC_DIR)
42
+ }
43
+ function recordFileNameOf(packageName) {
44
+ return `${String(packageName).replace(/[^A-Za-z0-9._-]+/gu, '_')}.json`
45
+ }
46
+ function sourceSpecRecordPath(home, packageName) {
47
+ return join(sourceSpecRecordDir(home), recordFileNameOf(packageName))
48
+ }
49
+
50
+ /** 读 JSON(读不到/坏文件都返回 null,绝不抛)。 */
51
+ function readJsonSafe(file) {
52
+ try {
53
+ return JSON.parse(readFileSync(file, 'utf8'))
54
+ } catch {
55
+ return null
56
+ }
57
+ }
58
+
59
+ /** 这个规格该不该交给包管理器**真装**(`link:` 不算:那是显式开发式安装)。 */
60
+ function isRealInstallSpec(spec) {
61
+ const canonical = canonicalSourceSpec(spec)
62
+ if (canonical === null || canonical === '') return false
63
+ if (LINK_SPEC_RE.test(canonical)) return false
64
+ return REAL_INSTALLABLE_RE.test(canonical)
65
+ }
66
+
67
+ /** 这个规格是不是 `link:`(显式开发式安装 / 本地目录的正常路径)。 */
68
+ function isLinkSourceSpec(spec) {
69
+ const canonical = canonicalSourceSpec(spec)
70
+ return canonical !== null && LINK_SPEC_RE.test(canonical)
71
+ }
72
+
73
+ /**
74
+ * 安装通道带回的「来源」(`{ spec, origin, kind }`)→ `ensurePersisted` 的入参。
75
+ * 空/没有规格 ⇒ **空对象**(调用方的形状与默认值与接线前逐字一致);有规格 ⇒ 三个字段一次给全,
76
+ * 收口才好判"这份物是取样通道铺的(sampled)还是包管理器装的(pnpm)"。
77
+ */
78
+ function sourcePersistOptions(source) {
79
+ if (source === null || source === undefined || typeof source !== 'object') return {}
80
+ const spec = typeof source.spec === 'string' && source.spec !== '' ? source.spec : null
81
+ if (spec === null) return {}
82
+ return { sourceSpec: spec, sourceOrigin: source.origin ?? null, sourceKind: source.kind ?? null }
83
+ }
84
+
85
+ /** 读某包的来源规格记录(没有/坏文件返回 null)。 */
86
+ function readSourceSpec({ packageName, home = null } = {}) {
87
+ if (typeof packageName !== 'string' || packageName === '') return null
88
+ const record = readJsonSafe(sourceSpecRecordPath(home, packageName))
89
+ if (record === null || typeof record !== 'object') return null
90
+ if (typeof record.spec !== 'string' || record.spec === '') return null
91
+ return record
92
+ }
93
+
94
+ /** 列出全部记录(体检/报告用)。 */
95
+ function listSourceSpecs({ home = null } = {}) {
96
+ const dir = sourceSpecRecordDir(home)
97
+ try {
98
+ return readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => ({ file: join(dir, f), record: readJsonSafe(join(dir, f)) }))
99
+ } catch {
100
+ return []
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 记下某包的真实来源规格(安装时写;**幂等**:规格与最近一次真装结论都没变 ⇒ 一个字节都不写)。
106
+ * `install` 传 `installSourceDependency` 的返回值(`{ ok, spec, error }`)时,最近一次结论一并记下。
107
+ * 返回 `{ ok, changed, file, record }`;任何失败都只如实返回,**绝不抛**(安装主路径不许被它带崩)。
108
+ */
109
+ function recordSourceSpec({ packageName, spec, kind = null, install = null, home = null } = {}) {
110
+ const empty = { ok: false, changed: false, file: null, record: null }
111
+ if (typeof packageName !== 'string' || packageName === '') return empty
112
+ const canonical = canonicalSourceSpec(spec)
113
+ if (canonical === null || canonical === '') return empty
114
+ const file = sourceSpecRecordPath(home, packageName)
115
+ const installSummary = install === null || install === undefined
116
+ ? null
117
+ : { ok: install.ok === true, error: typeof install.error === 'string' && install.error !== '' ? install.error.slice(0, 600) : null }
118
+ try {
119
+ const previous = readJsonSafe(file)
120
+ const sameSpec = typeof previous?.spec === 'string' && canonicalSourceSpec(previous.spec) === canonical
121
+ const sameInstall = installSummary === null
122
+ ? true
123
+ : previous?.lastInstall?.ok === installSummary.ok && (previous?.lastInstall?.error ?? null) === installSummary.error
124
+ if (sameSpec && sameInstall) return { ok: true, changed: false, file, record: previous }
125
+ const record = {
126
+ packageName,
127
+ spec: canonical,
128
+ kind: typeof kind === 'string' && kind !== '' ? kind : (previous?.kind ?? null),
129
+ recordedAt: new Date().toISOString(),
130
+ ...(installSummary === null ? {} : { lastInstall: { ...installSummary, at: new Date().toISOString() } }),
131
+ }
132
+ mkdirSync(dirname(file), { recursive: true })
133
+ writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, 'utf8')
134
+ return { ok: true, changed: true, file, record }
135
+ } catch (error) {
136
+ return { ...empty, file, reason: String(error?.message ?? error).slice(0, 300) }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * 忘掉某包的来源规格记录(**卸载即撤销**,与 `undeclareProfileDependency` 同一时机调用)。
142
+ * 删不掉时**退而求其次清空内容**(Windows 上 `<DSH_HOME>/plugin-console/` 下删除会静默落空,
143
+ * 与 junction / `*_tmp_*` 同族)—— 绝不留下指向已卸载包的陈旧规格。
144
+ */
145
+ function forgetSourceSpec({ packageName, home = null } = {}) {
146
+ if (typeof packageName !== 'string' || packageName === '') return { removed: false, emptied: false, file: null }
147
+ const file = sourceSpecRecordPath(home, packageName)
148
+ if (!existsSync(file)) return { removed: false, emptied: false, file }
149
+ try { rmSync(file, { force: true }) } catch {}
150
+ if (!existsSync(file)) return { removed: true, emptied: false, file }
151
+ let emptied = false
152
+ try {
153
+ writeFileSync(file, `${JSON.stringify({ packageName, forgotten: true, spec: null, note: '记录已作废;文件本身删除未生效,已清空以免留下陈旧规格' }, null, 2)}\n`, 'utf8')
154
+ emptied = readJsonSafe(file)?.spec === null
155
+ } catch { emptied = false }
156
+ return { removed: false, emptied, file }
157
+ }
158
+
159
+ /**
160
+ * 结构化「一键真装」动作(与 `suggestedPinAction` 同一套安全语义):
161
+ * `command` 只供展示/复制;执行侧只认 `payload.action`,**规格由服务端从记录里读**,
162
+ * payload 里没有 spec / 命令字符串的位置。
163
+ */
164
+ function suggestedRealInstallAction({ packageName, spec = null, profile = null, profileDir = null, reason = null } = {}) {
165
+ const name = profile ?? profileNameOf(profileDir)
166
+ const shown = canonicalSourceSpec(spec) ?? '<来源规格>'
167
+ return {
168
+ kind: 'real-install',
169
+ label: '一键真装(交给包管理器装依赖)',
170
+ command: `pnpm add ${shown} --registry https://registry.npmmirror.com`,
171
+ payload: { action: 'real-install', packageName, profile: name },
172
+ reason,
173
+ }
174
+ }
175
+
176
+ /**
177
+ * 真装结果 → 如实说法(**唯一承担者**)。三种形态各自把该说的话说完,绝不覆盖、绝不美化:
178
+ * · 成功:说清"按真实来源规格真装、依赖与 peer 由包管理器保证";
179
+ * · 失败 + 已回落 `link:`:说清「已装上(link 方式)· 真装失败:<原因> · 可一键真装」;
180
+ * · 失败 + 盘上什么都没有:说清"无可挂载(未改动清单)"——**不出现**任何"已装上"字样。
181
+ */
182
+ function composeSourceInstallNote({ packageName, install, plan = null } = {}) {
183
+ if (install === null || install === undefined || typeof install.ok !== 'boolean') return null
184
+ const spec = typeof install.spec === 'string' && install.spec !== '' ? install.spec : null
185
+ if (install.ok === true) {
186
+ return `${packageName}:已按真实来源规格真装(pnpm add ${spec ?? '(规格缺失)'})—— 依赖与 peer 由包管理器保证(与官方 dsh plugin add 同一条通道)`
187
+ }
188
+ const error = typeof install.error === 'string' && install.error !== '' ? install.error : '(原因未记录)'
189
+ if (plan?.form === 'link' && typeof plan.spec === 'string' && plan.spec !== '') {
190
+ return `${packageName}:已装上(link 方式)· 真装失败:${error} · 已按 link: 记录(${plan.spec}),缺失依赖由 link 自足性尽力补齐 · 可一键真装`
191
+ }
192
+ if (plan !== null && typeof plan?.spec === 'string' && plan.spec !== '') {
193
+ return `${packageName}:真装失败:${error};已按现有形态记录(${plan.spec})· 可一键真装`
194
+ }
195
+ return `${packageName}:真装失败:${error};磁盘上没有任何副本 —— 无可挂载(未改动清单)`
196
+ }
197
+
198
+ export {
199
+ SOURCE_SPEC_DIR,
200
+ REAL_INSTALLABLE_RE,
201
+ sourceSpecRecordDir,
202
+ sourceSpecRecordPath,
203
+ isRealInstallSpec,
204
+ isLinkSourceSpec,
205
+ sourcePersistOptions,
206
+ readSourceSpec,
207
+ listSourceSpecs,
208
+ recordSourceSpec,
209
+ forgetSourceSpec,
210
+ suggestedRealInstallAction,
211
+ composeSourceInstallNote,
212
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.40",
3
+ "version": "0.5.42",
4
4
  "description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
5
5
  "repository": {
6
6
  "type": "git",