@noob-stupid/dsh-plugin-console 0.3.53 → 0.3.54
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 +4 -4
- package/README.zh.md +5 -4
- package/lib/client.js +174 -29
- package/lib/index.js +459 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -308,7 +308,7 @@ MIT
|
|
|
308
308
|
|
|
309
309
|
The layered-refactor preview lives in its **own repository**: [**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
|
|
310
310
|
|
|
311
|
-
- **What it is**: the monolithic `lib/index.js` (
|
|
312
|
-
- **Verified**:
|
|
313
|
-
- **Not yet exercised**: real framework upgrade / rollback, restart guardian,
|
|
314
|
-
- **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.
|
|
311
|
+
- **What it is**: the monolithic `lib/index.js` (8547 lines) split into layers — `lib/index.js` **141 lines** + `lib/server/**` **38 modules**; **feature-equivalent to `0.3.54`**, maintainability only;
|
|
312
|
+
- **Verified**: 19 test suites green · 8 architecture-guard assertions · **route inventory identical** (`status` + response fields) against the stable build;
|
|
313
|
+
- **Not yet exercised**: real framework upgrade / rollback, restart guardian, component start/stop, real AI run, Gitee OAuth (~10–25% long-tail risk, subjective);
|
|
314
|
+
- **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.54`, or `main` of this repo.
|
package/README.zh.md
CHANGED
|
@@ -275,8 +275,9 @@ MIT
|
|
|
275
275
|
|
|
276
276
|
分层重构预览线在**独立仓库**:[**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
|
|
277
277
|
|
|
278
|
-
- **它是什么**:把单体 `lib/index.js`(
|
|
279
|
-
- **已验证**:
|
|
280
|
-
-
|
|
281
|
-
-
|
|
278
|
+
- **它是什么**:把单体 `lib/index.js`(8547 行)拆成分层结构 —— `lib/index.js` **141 行** + `lib/server/**` **38 个模块**,**功能与 `0.3.54` 等价**,只为可维护性;
|
|
279
|
+
- **已验证**:19 套测试全绿 · 8 条架构守卫断言 · 与稳定版逐条对打**路由清单一致**(`status` + 响应字段);
|
|
280
|
+
- **已实测**(2026-09-20 真装真卸演练):普通插件 / bundle 插件 / 无 npm 仓库 / 套装 / 技能 / 聚合仓库子包 / 仓库落地 / 服务器组件启停;
|
|
281
|
+
- **尚未实测**:真框架升级 / 真回滚、重启守护链路、AI 真跑、Gitee OAuth 回调(长尾风险主观估计 **10%~25%**);
|
|
282
|
+
- **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.54`,或本仓库 `main`。
|
|
282
283
|
- 请多反馈问题
|
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_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:20px;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:56px;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:104px}.pc_toolbarGroup{top:140px}.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)}";
|
|
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_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:20px;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:56px;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:104px}.pc_toolbarGroup{top:140px}.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)}";
|
|
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");
|
|
@@ -64,6 +64,10 @@ window.__ModuleLoader__.load({
|
|
|
64
64
|
modeFloat: "pc_modeFloat",
|
|
65
65
|
trashBtn: "pc_trashBtn",
|
|
66
66
|
consentRemember: "pc_consentRemember",
|
|
67
|
+
consentCard: "pc_consentCard",
|
|
68
|
+
consentCountdown: "pc_consentCountdown",
|
|
69
|
+
consentApprove: "pc_consentApprove",
|
|
70
|
+
progress: "pc_progress",
|
|
67
71
|
modalBackdrop: "pc_modalBackdrop",
|
|
68
72
|
modalCard: "pc_modalCard",
|
|
69
73
|
ghwrap: "pc_ghwrap",
|
|
@@ -242,10 +246,24 @@ window.__ModuleLoader__.load({
|
|
|
242
246
|
deleting: "删除中…",
|
|
243
247
|
deleteNote: "已删除插件并卸载包,正在刷新",
|
|
244
248
|
deleteBundleNote: "已移除插件所属 bundle,重启服务后生效",
|
|
249
|
+
pendingRestartTag: "已安装·重启后生效",
|
|
250
|
+
pendingRestartHint: "包已装好,重启服务后才会出现在上面的插件列表里;现在删除可直接撤回这次安装",
|
|
251
|
+
pendingRestartDelete: "撤销这次安装(删补丁行 / bundles 清单 / 包目录)",
|
|
252
|
+
pendingRestartNote: "已撤销这次安装(补丁行、bundles 清单、包目录均已清理)",
|
|
253
|
+
pendingRestartPartial: "撤销未完全生效,请按提示手动处理",
|
|
245
254
|
aiConsentText: "常规安装通道均已尝试失败。下一步将让本地 AI 接管安装——这会调用 DeepSeek API 模型,可能产生 API 费用。是否继续?",
|
|
246
255
|
aiConsentNotifyTitle: "插件安装需要授权",
|
|
247
256
|
aiConsentApprove: "同意,继续(可能产生费用)",
|
|
248
257
|
aiConsentDecline: "取消",
|
|
258
|
+
aiConsentCardTitle: "⚠ 需要你的授权:本地 AI 兜底",
|
|
259
|
+
aiConsentCountdown: "剩余",
|
|
260
|
+
aiConsentTimeoutNote: "超过 {min} 分钟不做选择将自动取消(不会产生任何费用)",
|
|
261
|
+
aiConsentLastError: "确定性通道最后一个错误",
|
|
262
|
+
aiConsentTopHint: "⚠ 有安装任务在等你授权:确定性安装通道都失败了,可使用本地 AI 兜底(会调用模型 API 产生费用)——请在安装进度处选择「同意 / 取消」",
|
|
263
|
+
progressSubpackage: "正在装第 {index}/{total} 个子包:{name}",
|
|
264
|
+
progressSuiteClone: "正在拉取第 {index}/{total} 个子模块:{name}",
|
|
265
|
+
progressSuiteAssemble: "正在装配第 {index}/{total} 个子模块:{name}",
|
|
266
|
+
progressDone: "已完成(共 {total} 个)",
|
|
249
267
|
aiFallbackLabel: "AI 兜底",
|
|
250
268
|
aiEmpowerLabel: "AI 赋能",
|
|
251
269
|
aiEmpowerTitle: "AI 赋能:读文档自动规划并部署组件;识别到服务器类组件后自动加启动/停止按钮",
|
|
@@ -625,10 +643,24 @@ window.__ModuleLoader__.load({
|
|
|
625
643
|
deleting: "Deleting…",
|
|
626
644
|
deleteNote: "Plugin deleted and package uninstalled; refreshing",
|
|
627
645
|
deleteBundleNote: "Owning bundle removed; takes effect after service restart",
|
|
646
|
+
pendingRestartTag: "Installed · after restart",
|
|
647
|
+
pendingRestartHint: "The package is installed and will show up in the list above after a restart; deleting now revokes this install",
|
|
648
|
+
pendingRestartDelete: "Revoke this install (drop patch row, bundles entry and package dir)",
|
|
649
|
+
pendingRestartNote: "Install revoked (patch row, bundles entry and package dir cleaned)",
|
|
650
|
+
pendingRestartPartial: "Revoke incomplete — handle the leftovers manually as reported",
|
|
628
651
|
aiConsentText: "All regular install channels failed. Next, the local AI takes over the install, which calls a DeepSeek API model and may incur API costs. Continue?",
|
|
629
652
|
aiConsentNotifyTitle: "Plugin install needs approval",
|
|
630
653
|
aiConsentApprove: "Agree, continue (may incur costs)",
|
|
631
654
|
aiConsentDecline: "Cancel",
|
|
655
|
+
aiConsentCardTitle: "⚠ Approval needed: local AI fallback",
|
|
656
|
+
aiConsentCountdown: "Time left",
|
|
657
|
+
aiConsentTimeoutNote: "No choice within {min} minutes cancels it automatically (no cost at all)",
|
|
658
|
+
aiConsentLastError: "Last error from the deterministic channels",
|
|
659
|
+
aiConsentTopHint: "⚠ An install is waiting for your approval: every deterministic channel failed, and the local AI fallback (which calls the model API and may incur costs) needs your consent — choose Agree / Cancel in the install progress area",
|
|
660
|
+
progressSubpackage: "Installing subpackage {index}/{total}: {name}",
|
|
661
|
+
progressSuiteClone: "Cloning submodule {index}/{total}: {name}",
|
|
662
|
+
progressSuiteAssemble: "Assembling submodule {index}/{total}: {name}",
|
|
663
|
+
progressDone: "Completed ({total} total)",
|
|
632
664
|
aiFallbackLabel: "AI fallback",
|
|
633
665
|
aiEmpowerLabel: "AI Empower",
|
|
634
666
|
aiEmpowerTitle: "AI Empower: read docs, plan and deploy a component automatically; server-type components get start/stop buttons",
|
|
@@ -2430,6 +2462,12 @@ window.__ModuleLoader__.load({
|
|
|
2430
2462
|
call("/plugin-console/install-status", { jobId }).then(
|
|
2431
2463
|
(data) => {
|
|
2432
2464
|
setJobs((prev) => ({ ...prev, [jobId]: data }));
|
|
2465
|
+
// 授权请求必须"看得见"(2026-09-20 真装实测事故:面板只把 stage 变成
|
|
2466
|
+
// ai-consent,用户不知道要做什么 → 干等 10 分钟被超时取消)。顶部消息先说一句,
|
|
2467
|
+
// 详情卡在下面的安装进度区(同一位置的醒目卡片 + 倒计时 + 同意/取消)。
|
|
2468
|
+
if (data.status === "installing" && data.stage === "ai-consent" && data.aiConsent?.pending === true && !aiAutoDeclinedRef.current[jobId]) {
|
|
2469
|
+
setMessage(t("aiConsentTopHint"));
|
|
2470
|
+
}
|
|
2433
2471
|
// 需要用户授权时(面板页后台/失焦也能看到):系统通知 + 标题闪烁提示
|
|
2434
2472
|
if (data.status === "installing" && data.stage === "ai-consent" && aiFallback && !aiRemember && !aiAutoDeclinedRef.current[jobId]) {
|
|
2435
2473
|
aiAutoDeclinedRef.current[jobId] = true;
|
|
@@ -2514,6 +2552,33 @@ window.__ModuleLoader__.load({
|
|
|
2514
2552
|
(error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
|
|
2515
2553
|
);
|
|
2516
2554
|
};
|
|
2555
|
+
/** 安装进度文案:服务端 progress{channel,phase,index,total,name,done} → 一行人话。
|
|
2556
|
+
* 2026-09-20 真装实测:11 个子包的聚合仓库跑 19 分钟,面板只有"安装中"——用户不知道
|
|
2557
|
+
* 在第几个、还剩几个、当前装的是谁。这里把服务端下发的进度翻成"第 i/n 个:<名字>"。 */
|
|
2558
|
+
const progressText = (progress) => {
|
|
2559
|
+
if (progress === null || progress === undefined) return "";
|
|
2560
|
+
if (progress.done === true) return t("progressDone").replace("{total}", String(progress.total ?? 0));
|
|
2561
|
+
const key = progress.channel === "suite"
|
|
2562
|
+
? (progress.phase === "clone" ? "progressSuiteClone" : "progressSuiteAssemble")
|
|
2563
|
+
: "progressSubpackage";
|
|
2564
|
+
return t(key)
|
|
2565
|
+
.replace("{index}", String(progress.index ?? 0))
|
|
2566
|
+
.replace("{total}", String(progress.total ?? 0))
|
|
2567
|
+
.replace("{name}", progress.name ?? "");
|
|
2568
|
+
};
|
|
2569
|
+
/** 授权倒计时(剩余 mm:ss):服务端只给请求时间 + 超时上限,剩余量在前端算。
|
|
2570
|
+
* 轮询每 2s 触发一次重渲染,倒计时随之刷新,不需要额外的定时器。 */
|
|
2571
|
+
const consentRemaining = (job) => {
|
|
2572
|
+
const consent = job?.aiConsent ?? null;
|
|
2573
|
+
if (consent === null || consent.pending !== true || typeof consent.since !== "number") return null;
|
|
2574
|
+
const timeoutMs = typeof consent.timeoutMs === "number" && consent.timeoutMs > 0 ? consent.timeoutMs : 600000;
|
|
2575
|
+
const left = Math.max(0, Math.round((consent.since + timeoutMs - Date.now()) / 1000));
|
|
2576
|
+
return Math.floor(left / 60) + ":" + String(left % 60).padStart(2, "0");
|
|
2577
|
+
};
|
|
2578
|
+
const consentTimeoutMinutes = (job) => {
|
|
2579
|
+
const timeoutMs = job?.aiConsent?.timeoutMs;
|
|
2580
|
+
return String(Math.max(1, Math.round((typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : 600000) / 60000)));
|
|
2581
|
+
};
|
|
2517
2582
|
/** 软件源管理:打开时拉取当前配置。 */
|
|
2518
2583
|
const openSources = () => {
|
|
2519
2584
|
setSourcesOpen(true);
|
|
@@ -3121,7 +3186,62 @@ window.__ModuleLoader__.load({
|
|
|
3121
3186
|
const root = section.head !== null ? section.head.key.slice("family-".length) : null;
|
|
3122
3187
|
if (root === null || familyCollapsed[root] !== true) familySectionEls.push(...section.cards);
|
|
3123
3188
|
}
|
|
3124
|
-
|
|
3189
|
+
// 已安装但尚未生效(bundle 型要重启才被加载;对应服务端 /state 的 pendingRestart):
|
|
3190
|
+
// 演练实测(2026-09-20)装完这类插件时 entryId 为 null、loader 里也没有新条目 ——
|
|
3191
|
+
// 用户以为没装上,删除按钮也无处可点。这里渲染成同样的行 + 「已安装·重启后生效」徽标,
|
|
3192
|
+
// 行上的删除按钮按 jobId 撤销这次安装(服务端删补丁行 / bundles 清单 / 包目录)。
|
|
3193
|
+
const pendingCards = (state.status === "ready" ? state.data.pendingRestart ?? [] : [])
|
|
3194
|
+
.filter((job) => installedQueryNorm === "" || [job.packageName, job.repo, job.jobId]
|
|
3195
|
+
.some((value) => String(value ?? "").toLowerCase().includes(installedQueryNorm)))
|
|
3196
|
+
.map((job) => el("li", { key: "pending-" + job.jobId, className: styles.row },
|
|
3197
|
+
el("div", { className: styles.rowTop },
|
|
3198
|
+
el("strong", { className: styles.name, title: job.packageName }, job.packageName),
|
|
3199
|
+
el("span", { className: styles.tag, "data-pending": "true", title: t("pendingRestartHint") }, t("pendingRestartTag")),
|
|
3200
|
+
),
|
|
3201
|
+
el("div", { className: styles.meta },
|
|
3202
|
+
el("code", null, job.jobId),
|
|
3203
|
+
job.repo ? el("code", null, job.repo) : null,
|
|
3204
|
+
),
|
|
3205
|
+
el("div", { className: styles.rowTop, style: { justifyContent: "space-between" } },
|
|
3206
|
+
el("p", { className: styles.desc }, t("pendingRestartHint")),
|
|
3207
|
+
el("div", { className: styles.rowTop },
|
|
3208
|
+
el("button", {
|
|
3209
|
+
type: "button",
|
|
3210
|
+
className: styles.trashBtn,
|
|
3211
|
+
title: t("pendingRestartDelete"),
|
|
3212
|
+
"aria-label": t("pendingRestartDelete"),
|
|
3213
|
+
disabled: deleteBusy === job.jobId,
|
|
3214
|
+
onClick: () => {
|
|
3215
|
+
if (confirmDeleteRowId === job.jobId) {
|
|
3216
|
+
setDeleteBusy(job.jobId);
|
|
3217
|
+
call("/plugin-console/uninstall", { jobId: job.jobId }).then(
|
|
3218
|
+
(data) => {
|
|
3219
|
+
setConfirmDeleteRowId(null);
|
|
3220
|
+
setMessage(data && data.warn
|
|
3221
|
+
? t("pendingRestartPartial") + ":" + data.warn
|
|
3222
|
+
: t("pendingRestartNote") + ":" + (data?.packageName ?? "") + (data?.uninstallError ? "(包卸载警告:" + data.uninstallError + ")" : ""));
|
|
3223
|
+
refresh();
|
|
3224
|
+
},
|
|
3225
|
+
(error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
|
|
3226
|
+
).finally(() => setDeleteBusy(null));
|
|
3227
|
+
} else {
|
|
3228
|
+
setConfirmDeleteRowId(job.jobId);
|
|
3229
|
+
window.setTimeout(() => setConfirmDeleteRowId((v) => (v === job.jobId ? null : v)), 3000);
|
|
3230
|
+
}
|
|
3231
|
+
},
|
|
3232
|
+
}, deleteBusy === job.jobId
|
|
3233
|
+
? t("deleting")
|
|
3234
|
+
: confirmDeleteRowId === job.jobId
|
|
3235
|
+
? t("confirmDelete")
|
|
3236
|
+
: el("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
|
|
3237
|
+
el("polyline", { points: "3 6 5 6 21 6" }),
|
|
3238
|
+
el("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }),
|
|
3239
|
+
el("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
|
|
3240
|
+
el("line", { x1: "14", y1: "11", x2: "14", y2: "17" }))),
|
|
3241
|
+
),
|
|
3242
|
+
),
|
|
3243
|
+
));
|
|
3244
|
+
const rows = state.status === "ready" ? [...nonFamilyCards, ...familySectionEls, ...pendingCards] : [];
|
|
3125
3245
|
const marketShown = market === null || market.status !== "ready"
|
|
3126
3246
|
? []
|
|
3127
3247
|
: officialOnly
|
|
@@ -4079,21 +4199,36 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
4079
4199
|
: null,
|
|
4080
4200
|
Object.values(jobs).filter((job) => job.status === "installing").map((job) => {
|
|
4081
4201
|
if (job.stage === "ai-consent") {
|
|
4082
|
-
//
|
|
4083
|
-
//
|
|
4084
|
-
|
|
4085
|
-
|
|
4202
|
+
// 授权请求就摆在**安装进度所在位置**(用户盯着进度看,授权提示不能只在模态框里)。
|
|
4203
|
+
// 这块卡片是要显眼的:警告色描边 + 倒计时 + 同意/取消,而不是一行小字。
|
|
4204
|
+
// 关掉兜底或勾了"不再提醒"时给出对应说明(这两种情况前端会自动同意/取消)。
|
|
4205
|
+
const autoNote = !aiFallback ? t("aiFallbackOff") : (aiRemember ? t("aiRememberNote") : null);
|
|
4206
|
+
const remain = consentRemaining(job);
|
|
4207
|
+
const lastError = job.aiConsent?.lastError ?? null;
|
|
4208
|
+
const progressLine = progressText(job.progress);
|
|
4209
|
+
return el("div", { key: job.jobId, className: styles.consentCard },
|
|
4086
4210
|
el("div", { className: styles.rowTop },
|
|
4087
4211
|
el("span", { className: styles.spinner }),
|
|
4088
|
-
el("strong", { className: styles.name }, t("
|
|
4089
|
-
|
|
4212
|
+
el("strong", { className: styles.name }, t("aiConsentCardTitle")),
|
|
4213
|
+
remain !== null ? el("span", { className: styles.consentCountdown }, t("aiConsentCountdown") + " " + remain) : null),
|
|
4214
|
+
el("p", { className: styles.message }, t("installingLocal") + ":" + (job.packageName ?? job.repo)),
|
|
4215
|
+
progressLine !== "" ? el("p", { className: styles.progress }, progressLine) : null,
|
|
4216
|
+
el("p", { className: styles.message }, t("aiConsentText")),
|
|
4217
|
+
lastError !== null ? el("p", { className: styles.note }, t("aiConsentLastError") + ":" + lastError) : null,
|
|
4218
|
+
remain !== null ? el("p", { className: styles.note }, t("aiConsentTimeoutNote").replace("{min}", consentTimeoutMinutes(job))) : null,
|
|
4219
|
+
autoNote !== null ? el("p", { className: styles.note }, autoNote) : null,
|
|
4220
|
+
el("div", { className: styles.rowTop },
|
|
4221
|
+
el("button", { type: "button", className: styles.consentApprove, onClick: () => aiConsent(job.jobId, true) }, t("aiConsentApprove")),
|
|
4222
|
+
el("button", { type: "button", className: styles.toggle, onClick: () => aiConsent(job.jobId, false) }, t("aiConsentDecline"))));
|
|
4090
4223
|
}
|
|
4091
4224
|
const stageKey = job.stage === "preparing" ? "stagePreparing" : job.stage === "configuring" ? "stageConfiguring" : job.stage === "repairing" ? "stageRepairing" : "stageInstalling";
|
|
4092
4225
|
const elapsed = Math.max(0, Math.round((Date.now() - (job.startedAt ?? Date.now())) / 1000));
|
|
4226
|
+
const progressLine = progressText(job.progress);
|
|
4093
4227
|
return el("div", { key: job.jobId, className: styles.detail },
|
|
4094
4228
|
el("div", { className: styles.rowTop },
|
|
4095
4229
|
el("span", { className: styles.spinner }),
|
|
4096
4230
|
el("strong", { className: styles.name }, t("installingLocal") + ":" + (job.packageName ?? job.repo))),
|
|
4231
|
+
progressLine !== "" ? el("p", { className: styles.progress }, progressLine) : null,
|
|
4097
4232
|
el("p", { className: styles.status },
|
|
4098
4233
|
t("stageLabel") + ":" + t(stageKey) + " · " + t("elapsed") + " " + elapsed + "s"));
|
|
4099
4234
|
}),
|
|
@@ -4224,7 +4359,7 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
4224
4359
|
? el("div", { className: styles.detail },
|
|
4225
4360
|
el("p", { className: styles.status, "data-error": "true" }, t("error")),
|
|
4226
4361
|
el("button", { type: "button", className: styles.toggle, onClick: refresh }, t("retry")))
|
|
4227
|
-
: state.data.entries.length === 0
|
|
4362
|
+
: state.data.entries.length === 0 && pendingCards.length === 0
|
|
4228
4363
|
? el("p", { className: styles.status }, t("empty"))
|
|
4229
4364
|
: installedQueryNorm !== "" && rows.length === 0
|
|
4230
4365
|
? el("p", { className: styles.status }, t("noMatch"))
|
|
@@ -4257,26 +4392,36 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
4257
4392
|
(() => {
|
|
4258
4393
|
// 最上层模态框:需要 AI 兜底授权时无论如何都让用户看到
|
|
4259
4394
|
const consentJob = Object.values(jobs).find((job) => job.status === "installing" && job.stage === "ai-consent" && aiFallback && !aiRemember);
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
el("
|
|
4278
|
-
|
|
4279
|
-
|
|
4395
|
+
if (consentJob === undefined) return null;
|
|
4396
|
+
// 倒计时与进度也放进模态框:用户在模态框上做决定,不用回去翻进度区
|
|
4397
|
+
const remain = consentRemaining(consentJob);
|
|
4398
|
+
const lastError = consentJob.aiConsent?.lastError ?? null;
|
|
4399
|
+
const progressLine = progressText(consentJob.progress);
|
|
4400
|
+
return el("div", { className: styles.modalBackdrop },
|
|
4401
|
+
el("div", { className: styles.modalCard },
|
|
4402
|
+
el("strong", { className: styles.name }, t("aiConsentCardTitle")),
|
|
4403
|
+
el("p", { className: styles.message }, t("installingLocal") + ":" + (consentJob.packageName ?? consentJob.repo)),
|
|
4404
|
+
progressLine !== "" ? el("p", { className: styles.progress }, progressLine) : null,
|
|
4405
|
+
remain !== null
|
|
4406
|
+
? el("p", { className: styles.message, "data-warn": "true" },
|
|
4407
|
+
t("aiConsentCountdown") + " " + remain + " · " + t("aiConsentTimeoutNote").replace("{min}", consentTimeoutMinutes(consentJob)))
|
|
4408
|
+
: null,
|
|
4409
|
+
el("p", { className: styles.message }, t("aiConsentText")),
|
|
4410
|
+
lastError !== null ? el("p", { className: styles.note }, t("aiConsentLastError") + ":" + lastError) : null,
|
|
4411
|
+
el("label", { className: styles.consentRemember },
|
|
4412
|
+
el("input", {
|
|
4413
|
+
type: "checkbox",
|
|
4414
|
+
checked: aiRemember,
|
|
4415
|
+
onChange: (event) => {
|
|
4416
|
+
const checked = event.currentTarget.checked;
|
|
4417
|
+
setAiRemember(checked);
|
|
4418
|
+
try { localStorage.setItem("pc-ai-remember", checked ? "1" : "0"); } catch {}
|
|
4419
|
+
},
|
|
4420
|
+
}),
|
|
4421
|
+
t("aiConsentRemember")),
|
|
4422
|
+
el("div", { className: styles.rowTop },
|
|
4423
|
+
el("button", { type: "button", className: styles.consentApprove, onClick: () => { try { localStorage.setItem("pc-ai-remember", aiRemember ? "1" : "0"); } catch {} aiConsent(consentJob.jobId, true); } }, t("aiConsentApprove")),
|
|
4424
|
+
el("button", { type: "button", className: styles.toggle, onClick: () => aiConsent(consentJob.jobId, false) }, t("aiConsentDecline")))));
|
|
4280
4425
|
})(),
|
|
4281
4426
|
sourcesOpen
|
|
4282
4427
|
? el("div", { className: styles.modalBackdrop },
|
package/lib/index.js
CHANGED
|
@@ -591,6 +591,250 @@ async function removeBundleFromManifest(profileDir, bundlePkg) {
|
|
|
591
591
|
})
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
+
// ============================================================
|
|
595
|
+
// 撤销「已安装但尚未生效」的安装(POST /uninstall { jobId } 的落地实现)
|
|
596
|
+
//
|
|
597
|
+
// 为什么需要(2026-09-20 真装真卸演练实测):面板装完一个 bundle 型插件后 /install 返回 entryId: null、
|
|
598
|
+
// GET /state 里**新增 loader 条目 = 0**(bundle 型要重启才被加载),而 /uninstall 只按**运行中** loader
|
|
599
|
+
// 条目查找 → 恒 404「没有名为 X 的插件条目」——刚装错的插件在重启前无法从面板卸载,用户只能手改 patch
|
|
600
|
+
// + package.json + 删 node_modules 才能撤回。安装任务里其实记着撤销所需的信息:
|
|
601
|
+
// job.packageName(装成了什么)/ job.bundle(是否注册进 dsh.profile.bundles)/ job.entryId(追加的补丁行)。
|
|
602
|
+
//
|
|
603
|
+
// ★ 三个子项各自 try/catch + **回读核实**,绝不 throw:补丁行 / bundles 清单 / 包目录。删除类操作在本机
|
|
604
|
+
// 某些环境下会**静默落空**(不抛错、目录仍在,见 removeDirVerified 注释),删完直接报成功就是对用户撒谎。
|
|
605
|
+
// ============================================================
|
|
606
|
+
|
|
607
|
+
/** 插件控制台自身的包名(撤销分支禁止删自己;@deepseek-ai/* 由通用护栏拒绝)。 */
|
|
608
|
+
const CONSOLE_PACKAGE = name
|
|
609
|
+
|
|
610
|
+
/** profile 内 node_modules/<pkg> 的目录(作用域包按 / 分段,与 pnpm 布局一致)。 */
|
|
611
|
+
function packageDirIn(profileDir, packageName) {
|
|
612
|
+
const name = String(packageName)
|
|
613
|
+
const segments = name.startsWith('@') ? name.split('/') : [name]
|
|
614
|
+
return join(profileDir, 'node_modules', ...segments)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** profile 的 package.json 是否还把这个包当依赖(pnpm add 会写进 dependencies)。 */
|
|
618
|
+
async function manifestRefsPackage(profileDir, packageName) {
|
|
619
|
+
try {
|
|
620
|
+
const manifest = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8'))
|
|
621
|
+
return ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']
|
|
622
|
+
.some((key) => manifest?.[key] !== null && typeof manifest?.[key] === 'object'
|
|
623
|
+
&& Object.prototype.hasOwnProperty.call(manifest[key], packageName))
|
|
624
|
+
} catch {
|
|
625
|
+
return false
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** 这个包是否还需要一次 pnpm remove:目录还在盘上,或 manifest 还引用它。
|
|
630
|
+
* 两者都不成立时拉 pnpm 只会换来一句没有信息量的 `Command failed: corepack pnpm remove …`(演练实测)
|
|
631
|
+
* —— 但 manifest 仍引用时必须跑,否则 package.json 会留下指向空目录的幽灵依赖。 */
|
|
632
|
+
async function needsPnpmRemove(profileDir, packageName) {
|
|
633
|
+
return existsSync(packageDirIn(profileDir, packageName)) || await manifestRefsPackage(profileDir, packageName)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** bundle 包自带 patch(node_modules/<pkg>/cordis.patch.yml)里**归属该包**的 insert 行 id。
|
|
637
|
+
* 为什么要它:这类行由 bundle 的补丁在下次启动时组合进树,profile 补丁里没有 insert 块,但可能留着
|
|
638
|
+
* 它们的 `- id: X` + `disabled: true` 覆盖块(聚合包完整性检查自动禁用 / 用户手动停用)。
|
|
639
|
+
* 只认模块名等于该包或它的子路径的行 —— 聚合包补丁里常引用**别人家**的包(如 @deepseek-ai/dsh-root),
|
|
640
|
+
* 那些行的覆盖块不属于本次撤销,动了就是越界。 */
|
|
641
|
+
async function bundleOwnRowIds(profileDir, packageName) {
|
|
642
|
+
const ids = new Set()
|
|
643
|
+
try {
|
|
644
|
+
const text = await readFile(join(packageDirIn(profileDir, packageName), 'cordis.patch.yml'), 'utf8')
|
|
645
|
+
for (const [rowId, moduleName] of parseInsertNames(text)) {
|
|
646
|
+
if (moduleName === packageName || String(moduleName).startsWith(`${packageName}/`)) ids.add(rowId)
|
|
647
|
+
}
|
|
648
|
+
} catch {}
|
|
649
|
+
return ids
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* 撤销一个「已安装但未生效」的安装任务。三个子项各自 try/catch,成败如实汇报;本函数不抛错。
|
|
654
|
+
* 返回 { packageName, bundle, rowIds, verified: { patchClean, bundlesClean, packageGone }, warn, uninstallError }
|
|
655
|
+
*/
|
|
656
|
+
export async function revokePendingInstall(job, { profileDir, patchPath, pnpmRemove: removeFn }) {
|
|
657
|
+
const packageName = typeof job?.packageName === 'string' ? job.packageName.trim() : ''
|
|
658
|
+
const isBundle = job?.bundle === true
|
|
659
|
+
// 兜底(路由已先拦):没有包名就什么都别碰 —— 靠 entryId 单点删除可能误伤别的包
|
|
660
|
+
if (packageName === '') {
|
|
661
|
+
return {
|
|
662
|
+
packageName, bundle: isBundle, rowIds: [],
|
|
663
|
+
verified: { patchClean: false, bundlesClean: false, packageGone: false },
|
|
664
|
+
warn: '该安装任务没有记录包名,未执行任何删除(补丁行 / bundles 清单 / 包目录都未处理)',
|
|
665
|
+
uninstallError: null,
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// ── a) 补丁行:删掉这次安装注册的 insert 行 + 它的 disabled/forced 覆盖块 ──────────
|
|
669
|
+
const rowIds = new Set()
|
|
670
|
+
let patchClean = false
|
|
671
|
+
let patchProblem = null
|
|
672
|
+
try {
|
|
673
|
+
const before = await readPatchState(patchPath)
|
|
674
|
+
const declared = parseInsertNames(before.text) // insert 行 id → 模块名
|
|
675
|
+
if (typeof job?.entryId === 'string' && job.entryId !== '') {
|
|
676
|
+
const owner = declared.get(job.entryId)
|
|
677
|
+
// 补丁里这一行若明确属于**别的包**,绝不按 entryId 删(归属判定以补丁文本为准)
|
|
678
|
+
if (owner === undefined || owner === packageName) rowIds.add(job.entryId)
|
|
679
|
+
}
|
|
680
|
+
for (const [rowId, moduleName] of declared) if (moduleName === packageName) rowIds.add(rowId)
|
|
681
|
+
for (const rowId of await bundleOwnRowIds(profileDir, packageName)) rowIds.add(rowId)
|
|
682
|
+
for (const rowId of rowIds) {
|
|
683
|
+
if (before.inserts.includes(rowId)) await removeInsertRow(patchPath, rowId)
|
|
684
|
+
await removeDisableBlock(patchPath, rowId)
|
|
685
|
+
}
|
|
686
|
+
// 回读核实:目标行(insert / disabled / forced)都不在了,补丁文本也不该再出现这个包名
|
|
687
|
+
const after = await readPatchState(patchPath)
|
|
688
|
+
const leakedRows = [...rowIds].filter((id) => after.inserts.includes(id) || after.disables.includes(id) || after.forced.includes(id))
|
|
689
|
+
const leakedNames = [...parseInsertNames(after.text).values()].filter((name) => name === packageName)
|
|
690
|
+
patchClean = leakedRows.length === 0 && leakedNames.length === 0 && !String(after.text ?? '').includes(packageName)
|
|
691
|
+
if (!patchClean) patchProblem = `仍有残留行 ${[...new Set([...leakedRows, ...leakedNames])].join('、') || packageName}`
|
|
692
|
+
} catch (error) {
|
|
693
|
+
patchProblem = error instanceof Error ? error.message : String(error)
|
|
694
|
+
}
|
|
695
|
+
// ── b) bundles 清单:只删这一个包名(保留顺序、保留其余项)──────────────────────
|
|
696
|
+
let bundlesClean = !isBundle
|
|
697
|
+
let bundlesProblem = null
|
|
698
|
+
if (isBundle) {
|
|
699
|
+
try {
|
|
700
|
+
await removeBundleFromManifest(profileDir, packageName)
|
|
701
|
+
const manifest = JSON.parse(await readFile(join(profileDir, 'package.json'), 'utf8'))
|
|
702
|
+
bundlesClean = !(manifest?.dsh?.profile?.bundles ?? []).includes(packageName)
|
|
703
|
+
if (!bundlesClean) bundlesProblem = `dsh.profile.bundles 里仍有 ${packageName}`
|
|
704
|
+
} catch (error) {
|
|
705
|
+
bundlesProblem = error instanceof Error ? error.message : String(error)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// ── c) 包目录:pnpm remove(与安装同一管理器)──────────────────────────────────
|
|
709
|
+
const packageDir = packageDirIn(profileDir, packageName)
|
|
710
|
+
let uninstallError = null
|
|
711
|
+
if (await needsPnpmRemove(profileDir, packageName)) {
|
|
712
|
+
try {
|
|
713
|
+
await removeFn(profileDir, packageName)
|
|
714
|
+
} catch (error) {
|
|
715
|
+
uninstallError = error instanceof Error ? error.message : String(error)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const packageGone = !existsSync(packageDir)
|
|
719
|
+
// ── d) 核实汇总:任一子项没清干净 → ok 仍为 true,但必须带 warn 说清是哪项、路径在哪 ──
|
|
720
|
+
const verified = { patchClean, bundlesClean, packageGone }
|
|
721
|
+
const problems = []
|
|
722
|
+
if (!patchClean) problems.push(`补丁未清干净(${patchPath}):${patchProblem ?? '仍有残留行'}`)
|
|
723
|
+
if (!bundlesClean) problems.push(`bundles 清单未清干净(${join(profileDir, 'package.json')}):${bundlesProblem ?? `仍有 ${packageName}`}`)
|
|
724
|
+
if (!packageGone) problems.push(`包目录仍在(${packageDir})${uninstallError !== null ? `,pnpm remove 失败:${uninstallError}` : ''}`)
|
|
725
|
+
const warn = problems.length > 0 ? `撤销未完全生效 —— ${problems.join(';')};请手动处理后重试` : null
|
|
726
|
+
return { packageName, bundle: isBundle, rowIds: [...rowIds], verified, warn, uninstallError }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* 「已安装 · 重启后生效」清单:status === 'done'、未撤销、有包名,且该包**不在**运行中的 loader 条目里。
|
|
731
|
+
* bundle 型插件要重启才被加载(演练实测:装完新增 loader 条目 = 0),前端据此把这类任务渲染成带待重启
|
|
732
|
+
* 徽标的行,并允许按 jobId 撤销。entries 传 listEntries(ctx) 的结果(含子路径条目 → 归一到根包名再比)。
|
|
733
|
+
*/
|
|
734
|
+
function pendingRestartJobs(jobs, entries) {
|
|
735
|
+
const running = new Set()
|
|
736
|
+
for (const entry of entries ?? []) {
|
|
737
|
+
const moduleName = entry?.moduleName
|
|
738
|
+
if (typeof moduleName !== 'string' || moduleName === '' || moduleName.startsWith('cordis:')) continue
|
|
739
|
+
running.add(moduleName)
|
|
740
|
+
const root = packageNameOf(moduleName)
|
|
741
|
+
if (typeof root === 'string' && root !== '') running.add(root)
|
|
742
|
+
}
|
|
743
|
+
// 同一包名的多次安装/更新(安装表里会有多条 done 记录)只留最后一次:
|
|
744
|
+
// 否则一个插件在面板上会出现好几行「已安装·重启后生效」。
|
|
745
|
+
const latest = new Map()
|
|
746
|
+
for (const job of jobs ?? []) {
|
|
747
|
+
if (job?.status !== 'done' || job.revokedAt !== undefined) continue
|
|
748
|
+
if (typeof job.packageName !== 'string' || job.packageName.trim() === '') continue
|
|
749
|
+
if (running.has(job.packageName)) continue
|
|
750
|
+
latest.set(job.packageName, job)
|
|
751
|
+
}
|
|
752
|
+
return [...latest.values()].map((job) => ({
|
|
753
|
+
jobId: job.id,
|
|
754
|
+
repo: job.repo ?? null,
|
|
755
|
+
packageName: job.packageName,
|
|
756
|
+
bundle: job.bundle === true,
|
|
757
|
+
finishedAt: job.finishedAt ?? null,
|
|
758
|
+
}))
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** 把与这次删除对应的「已安装·重启后生效」任务记为已撤销(/state 的 pendingRestart 据此过滤)。
|
|
762
|
+
* 为什么两处都要调:无论按 entryId(重启后从列表删)还是按 jobId(重启前撤销)删掉一个包,它的安装任务
|
|
763
|
+
* 若还挂着 status==='done',/state 就会继续显示一行删不掉的幽灵行。同一包名的多条记录(装过又更新过)
|
|
764
|
+
* 一并作废 —— 它们指向同一个包。 */
|
|
765
|
+
function markJobsRevoked(packageName, rowId) {
|
|
766
|
+
for (const job of installJobs.values()) {
|
|
767
|
+
if (job.status !== 'done' || job.revokedAt !== undefined) continue
|
|
768
|
+
if (job.packageName === packageName || (typeof rowId === 'string' && rowId !== '' && job.entryId === rowId)) {
|
|
769
|
+
job.revokedAt = Date.now()
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* 按安装任务撤销「已安装但尚未生效」的安装(/uninstall 的 jobId 分支)。
|
|
776
|
+
* 安全护栏与 entry 分支完全一致:@deepseek-ai/* · isProtectedModule · 控制台自身。
|
|
777
|
+
*/
|
|
778
|
+
async function uninstallByJobId(ctx, res, jobId) {
|
|
779
|
+
const job = installJobs.get(jobId)
|
|
780
|
+
if (job === undefined) {
|
|
781
|
+
sendError(res, 404, `没有这个安装任务(jobId=${jobId})——无法撤销`)
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
if (job.revokedAt !== undefined) {
|
|
785
|
+
sendError(res, 400, `该安装任务已经撤销过了(${new Date(job.revokedAt).toISOString()}),无需重复操作`)
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
if (job.status !== 'done') {
|
|
789
|
+
sendError(res, 400, job.status === 'installing'
|
|
790
|
+
? `该安装任务还在进行中(stage=${job.stage ?? '?'}),完成后才能撤销`
|
|
791
|
+
: `该安装任务没有成功装成(status=${job.status}${job.error ? `:${job.error}` : ''}),没有可撤销的安装`)
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
const packageName = typeof job.packageName === 'string' ? job.packageName.trim() : ''
|
|
795
|
+
if (packageName === '') {
|
|
796
|
+
sendError(res, 400, '该安装任务没有记录包名(可能未装成、或已由现有聚合包提供),无法按任务撤销')
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
if (packageName.startsWith('@deepseek-ai/')) {
|
|
800
|
+
sendError(res, 403, `${packageName} 是 DSH 框架官方包,禁止删除`)
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
if (isProtectedModule(packageName)) {
|
|
804
|
+
sendError(res, 403, `${packageName} 属于宿主基础设施,禁止删除`)
|
|
805
|
+
return
|
|
806
|
+
}
|
|
807
|
+
const rowId = typeof job.entryId === 'string' && job.entryId !== '' ? job.entryId : deriveEntryId(packageName, new Set())
|
|
808
|
+
if (rowId === 'plugin-console' || packageName === CONSOLE_PACKAGE) {
|
|
809
|
+
sendError(res, 400, '不能删除插件控制台自身')
|
|
810
|
+
return
|
|
811
|
+
}
|
|
812
|
+
// 本分支只服务「已安装但尚未生效」:包名若已在运行中的 loader 条目里(重启已完成),撤销会留下
|
|
813
|
+
// 「包已删、模块还挂在内存里」的半状态 —— 如实拒绝并指路(按列表条目删除)。
|
|
814
|
+
if (pendingRestartJobs([job], listEntries(ctx)).length === 0) {
|
|
815
|
+
sendError(res, 400, `「${packageName}」已经在运行中的插件列表里(重启已完成)——请直接在列表里删除该条目,不必按 jobId 撤销`)
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
const patchPath = findPatchPath(ctx)
|
|
819
|
+
const profileDir = dirname(patchPath)
|
|
820
|
+
const result = await revokePendingInstall(job, { profileDir, patchPath, pnpmRemove })
|
|
821
|
+
// 只有包目录真的没了才算「这次安装已撤销」:此后不再出现在 /state 的 pendingRestart 里。
|
|
822
|
+
// 包还在盘上时不打这个标记 —— 保留待重启条目让用户能再点一次删除,比假装干净好。
|
|
823
|
+
if (result.verified.packageGone === true) markJobsRevoked(result.packageName, typeof job.entryId === 'string' ? job.entryId : null)
|
|
824
|
+
sendJson(res, 200, {
|
|
825
|
+
ok: true,
|
|
826
|
+
removed: 'pending-install',
|
|
827
|
+
jobId,
|
|
828
|
+
packageName: result.packageName,
|
|
829
|
+
bundle: result.bundle,
|
|
830
|
+
restart: false,
|
|
831
|
+
rowIds: result.rowIds,
|
|
832
|
+
verified: result.verified,
|
|
833
|
+
warn: result.warn,
|
|
834
|
+
uninstallError: result.uninstallError,
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
594
838
|
/** 用 corepack pnpm 移除包(与安装同一管理器与凭据抑制环境)。
|
|
595
839
|
* 注意:pnpm remove 不支持 --registry 选项,去掉避免静默失败。 */
|
|
596
840
|
export async function pnpmRemove(profileDir, packageName) {
|
|
@@ -632,6 +876,53 @@ function cleanupStalePackageDir(profileDir, packageName) {
|
|
|
632
876
|
return removed
|
|
633
877
|
}
|
|
634
878
|
|
|
879
|
+
/** 安装失败清场:把本次尝试过的候选包目录与 pnpm `_tmp_` 半成品一起清掉,并**如实汇报**清了什么、
|
|
880
|
+
* 什么没清掉。为什么需要(2026-09-20 真装演练):11 个子包的聚合仓库跑 19 分钟后失败,node_modules 里
|
|
881
|
+
* 留着 `@captain1275/dsh-full-stats_tmp_56272_2` 这类半成品和一个真包,而面板只报"安装失败"——
|
|
882
|
+
* 用户既不知道有东西被落盘,也不知道要不要手动清。删除一律走 removeDirVerified(本机 C 盘/`%TEMP%`
|
|
883
|
+
* 下 rmSync 会静默落空,不核实就会谎报干净)。 */
|
|
884
|
+
export function cleanupAttemptedCandidates(profileDir, candidates) {
|
|
885
|
+
const cleaned = []
|
|
886
|
+
const failed = []
|
|
887
|
+
for (const name of candidates) {
|
|
888
|
+
const dir = join(profileDir, 'node_modules', ...String(name).split('/'))
|
|
889
|
+
const parent = dirname(dir)
|
|
890
|
+
const base = basename(dir)
|
|
891
|
+
let touched = 0
|
|
892
|
+
if (existsSync(dir)) {
|
|
893
|
+
const result = removeDirVerified(dir)
|
|
894
|
+
if (result.ok) touched += 1
|
|
895
|
+
else failed.push({ name, path: dir, error: result.error })
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
for (const entry of readdirSync(parent)) {
|
|
899
|
+
if (!entry.startsWith(`${base}_tmp_`)) continue
|
|
900
|
+
const tmpPath = join(parent, entry)
|
|
901
|
+
const result = removeDirVerified(tmpPath)
|
|
902
|
+
if (result.ok) touched += 1
|
|
903
|
+
else failed.push({ name: `${name}(临时目录)`, path: tmpPath, error: result.error })
|
|
904
|
+
}
|
|
905
|
+
} catch {}
|
|
906
|
+
if (touched > 0) cleaned.push(name)
|
|
907
|
+
}
|
|
908
|
+
return { cleaned, failed }
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** 授权被拒/超时后的失败文案(纯函数,单测覆盖):说清为什么失败、清理了什么、什么没清掉。
|
|
912
|
+
* 时长取自 AI_CONSENT_TIMEOUT_MS —— 文案里的"10 分钟"不能与实际等待时间脱节。 */
|
|
913
|
+
export function aiConsentFailureText(decision, leftovers) {
|
|
914
|
+
const base = decision?.timeout === true
|
|
915
|
+
? `等待授权超时(${Math.round(AI_CONSENT_TIMEOUT_MS / 60000)} 分钟),已取消本地 AI 兜底(该操作会调用模型 API 产生费用)`
|
|
916
|
+
: '用户取消本地 AI 兜底(该操作会调用模型 API 产生费用)'
|
|
917
|
+
const cleaned = leftovers?.cleaned ?? []
|
|
918
|
+
const failed = leftovers?.failed ?? []
|
|
919
|
+
const cleanedNote = cleaned.length > 0 ? `;已清理本次落盘残留:${cleaned.join('、')}` : ''
|
|
920
|
+
const failedNote = failed.length > 0
|
|
921
|
+
? `;**有 ${failed.length} 项没能清理**(当前环境可能禁止删除,请手动删除):${failed.map((f) => f.path).join('、')}`
|
|
922
|
+
: ''
|
|
923
|
+
return `${base}${cleanedNote}${failedNote}`
|
|
924
|
+
}
|
|
925
|
+
|
|
635
926
|
/** 软件源配置文件(registry 列表,安装链按主→备依次尝试)。 */
|
|
636
927
|
const SOURCES_FILE = join(dshHome(), 'plugin-console-sources.json')
|
|
637
928
|
/** 敏感凭据单独落盘(避免与可分享配置混存):自定义搜索源 headers + Gitee secret/token。 */
|
|
@@ -2824,7 +3115,7 @@ async function ensureBundlePatchIntegrity(profileDir, pkgName, userPatchPath, tr
|
|
|
2824
3115
|
* 后台安装任务注册表:请求立即返回,安装继续在服务端执行;
|
|
2825
3116
|
* 面板通过 /install-status 轮询进度(stage + status),离开面板不中断。
|
|
2826
3117
|
*/
|
|
2827
|
-
const installJobs = new Map()
|
|
3118
|
+
export const installJobs = new Map()
|
|
2828
3119
|
let installJobSeq = 0
|
|
2829
3120
|
/** AI 赋能任务注册表(规划/执行均常驻服务端,面板轮询进度)。 */
|
|
2830
3121
|
const aiJobs = new Map()
|
|
@@ -2900,6 +3191,9 @@ function loadAiJobs() {
|
|
|
2900
3191
|
/** 静态插件索引内存缓存(/market-index 用)。 */
|
|
2901
3192
|
let marketIndexCache = null
|
|
2902
3193
|
|
|
3194
|
+
/** 本地 AI 兜底授权的等待上限(10 分钟):超时即视为用户未授权(见 runInstallJob 的 ai-consent 段)。 */
|
|
3195
|
+
const AI_CONSENT_TIMEOUT_MS = 600000
|
|
3196
|
+
|
|
2903
3197
|
function installJobView(job) {
|
|
2904
3198
|
return {
|
|
2905
3199
|
jobId: job.id,
|
|
@@ -2926,6 +3220,16 @@ function installJobView(job) {
|
|
|
2926
3220
|
suiteReport: job.suiteReport ?? null,
|
|
2927
3221
|
suiteNote: job.suiteNote ?? null,
|
|
2928
3222
|
hint: job.hint ?? null,
|
|
3223
|
+
// 子包级/套装级进度(2026-09-20 真装实测缺口:11 个子包的聚合仓库跑了 19 分钟,面板只显示
|
|
3224
|
+
// "安装中",用户不知道在装第几个、还剩几个)。子包通道来自 runInstallJob 候选循环维护的
|
|
3225
|
+
// candidateXxx;套装通道来自 runSuiteInstallJob 维护的 suiteProgress(clone/装配两阶段)。
|
|
3226
|
+
progress: job.suiteProgress
|
|
3227
|
+
? { channel: 'suite', ...job.suiteProgress }
|
|
3228
|
+
: job.candidateTotal > 0
|
|
3229
|
+
? { channel: 'subpackage', phase: 'install', index: job.candidateIndex ?? 0, total: job.candidateTotal, name: job.candidateName ?? null, done: job.candidateDone === true }
|
|
3230
|
+
: null,
|
|
3231
|
+
// 授权请求(等本地 AI 兜底同意):面板要显示倒计时 + 同意/取消,所以时间与最后错误一起下发
|
|
3232
|
+
aiConsent: { pending: job.aiPending != null, since: job.aiPendingSince ?? null, timeoutMs: job.aiConsentTimeoutMs ?? AI_CONSENT_TIMEOUT_MS, lastError: job.aiPending?.lastError ?? job.lastError ?? null },
|
|
2929
3233
|
}
|
|
2930
3234
|
}
|
|
2931
3235
|
|
|
@@ -3098,15 +3402,55 @@ function gitCloneUrls(repoFullName, source = 'github') {
|
|
|
3098
3402
|
return urls.length > 0 ? urls : [`https://github.com/${repoFullName}.git`]
|
|
3099
3403
|
}
|
|
3100
3404
|
|
|
3405
|
+
/**
|
|
3406
|
+
* 删除目录树并**核实删除结果**。理由:本机某些环境(受限令牌/沙箱/杀软占用)下 `rmSync` 会
|
|
3407
|
+
* **静默落空**——不抛错、目录仍在。删完直接报成功就是对用户撒谎(2026-09-20 多类型演练实测:
|
|
3408
|
+
* 同一个 `rmSync` 在 `D:\dsh\repos` 删得掉,在 `C:\Users\<user>\.dsh\...` 与 `%TEMP%` 下返回成功
|
|
3409
|
+
* 但目录原封不动;技能删不掉、残留清理假装清干净、套装子模块重试撞"目录非空"都是它引起的)。
|
|
3410
|
+
*/
|
|
3411
|
+
export function removeDirVerified(dir) {
|
|
3412
|
+
let lastError = null
|
|
3413
|
+
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
3414
|
+
try {
|
|
3415
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 120 })
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
lastError = error
|
|
3418
|
+
}
|
|
3419
|
+
if (!existsSync(dir)) return { ok: true, attempts: attempt }
|
|
3420
|
+
}
|
|
3421
|
+
return { ok: false, attempts: 2, error: lastError instanceof Error ? lastError.message : null }
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3424
|
+
/** 取 git 自己说的话(stderr 末两行)。`--quiet` 只静音进度,真实原因(HTTP 502/无法解析主机/
|
|
3425
|
+
* 认证失败)仍在 stderr 里;只报 `Command failed: git clone …` 等于没告诉用户任何信息。 */
|
|
3426
|
+
function gitErrorDetail(error) {
|
|
3427
|
+
const raw = typeof error?.stderr === 'string' && error.stderr.trim() !== ''
|
|
3428
|
+
? error.stderr
|
|
3429
|
+
: (typeof error?.message === 'string' ? error.message : '')
|
|
3430
|
+
return raw
|
|
3431
|
+
.split(/\r?\n/u)
|
|
3432
|
+
.map((line) => line.trim())
|
|
3433
|
+
.filter((line) => line !== '' && !/^Command failed/u.test(line))
|
|
3434
|
+
.slice(-2)
|
|
3435
|
+
.join(' | ')
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3101
3438
|
/** 逐条尝试的错误汇总(纯函数,单测覆盖):报**第一个**错误(真实原因)+ 尝试清单。
|
|
3102
3439
|
* 为什么要这样:多源重试时若第一次失败留下半成品目录,第二次会以
|
|
3103
3440
|
* `fatal: destination path '…' already exists and is not an empty directory` 失败,
|
|
3104
3441
|
* 旧代码把它当 lastError 抛出去 → 用户只看到"目录非空",真实原因(镜像/网络不可达)被完全掩盖。
|
|
3105
|
-
* 2026-09-20
|
|
3442
|
+
* 2026-09-20 另一位用户实测报的就是这句;同日演练又发现:**清理半成品本身失败**时(环境禁止删除
|
|
3443
|
+
* %TEMP%),第二个源的好戏只有"目录非空",必须把"清不掉"讲明白,否则用户永远猜不到真相。 */
|
|
3106
3444
|
export function summarizeCloneErrors(errors) {
|
|
3107
3445
|
const first = errors[0]
|
|
3108
|
-
const tried = errors.map((e) => (
|
|
3109
|
-
|
|
3446
|
+
const tried = errors.map((e) => (e.unclean === true
|
|
3447
|
+
? `${e.url}(目标目录清不掉,未重试)`
|
|
3448
|
+
: (/already exists and is not an empty directory/u.test(e.message) ? `${e.url}(目录非空)` : e.url))).join(';')
|
|
3449
|
+
const detail = gitErrorDetail(first)
|
|
3450
|
+
const uncleanNote = errors.some((e) => e.unclean === true)
|
|
3451
|
+
? ';注意:上一次失败留下的半成品目录无法清理(当前环境禁止删除),多源重试因此无效——请手动删除该目录后重试'
|
|
3452
|
+
: ''
|
|
3453
|
+
return `git clone 失败(首个错误:${first?.message ?? '未知'}${detail !== '' ? `;git 说:${detail}` : ''});已尝试 ${errors.length} 个源:${tried}${uncleanNote}`
|
|
3110
3454
|
}
|
|
3111
3455
|
|
|
3112
3456
|
/** git clone(镜像→直连;gitee 直连),返回 { url } 或抛错。 */
|
|
@@ -3116,7 +3460,13 @@ export async function gitCloneRepo(repo, dest, source = 'github', timeout = 1800
|
|
|
3116
3460
|
for (const [attempt, url] of urls.entries()) {
|
|
3117
3461
|
// 每次尝试前都清掉目标目录:上一次可能留下半成品(git 会先建目录再传输),
|
|
3118
3462
|
// 不清就会让第二次以"目录非空"失败并掩盖真实原因(见 summarizeCloneErrors 注释)。
|
|
3119
|
-
|
|
3463
|
+
// 关键是**核实**清理结果:清不掉就别再试下一个源了——那只会得到一条"目录非空",
|
|
3464
|
+
// 把第一个源的真实错误也一起搅浑(本机 %TEMP% 下删除被静默忽略时就是这样)。
|
|
3465
|
+
const cleared = removeDirVerified(dest)
|
|
3466
|
+
if (!cleared.ok) {
|
|
3467
|
+
errors.push({ url, message: `克隆目标目录无法清理(环境禁止删除):${dest}`, unclean: true })
|
|
3468
|
+
break
|
|
3469
|
+
}
|
|
3120
3470
|
try {
|
|
3121
3471
|
// eslint-disable-next-line no-await-in-loop
|
|
3122
3472
|
await execFileAsync('git', ['clone', '--depth', '1', '--quiet', url, dest], {
|
|
@@ -3126,7 +3476,7 @@ export async function gitCloneRepo(repo, dest, source = 'github', timeout = 1800
|
|
|
3126
3476
|
})
|
|
3127
3477
|
return { url, attempt: attempt + 1 }
|
|
3128
3478
|
} catch (error) {
|
|
3129
|
-
errors.push({ url, message: error?.message ?? String(error) })
|
|
3479
|
+
errors.push({ url, message: error?.message ?? String(error), stderr: error?.stderr })
|
|
3130
3480
|
}
|
|
3131
3481
|
}
|
|
3132
3482
|
throw new Error(summarizeCloneErrors(errors))
|
|
@@ -3187,7 +3537,7 @@ export function resolveInstallKind(requestKind, probeText) {
|
|
|
3187
3537
|
/** 套装安装(submodule 聚合仓库):照仓库 install.ps1/README 语义——
|
|
3188
3538
|
* clone 套装 → 手动镜像拉取子模块 → 按类型装配(bundle 插件含 Release tgz 兜底 / 普通插件 / 技能 / agent 预设)。
|
|
3189
3539
|
* 不执行第三方脚本本体(安全护栏:脚本型只读语义不运行)。 */
|
|
3190
|
-
async function runSuiteInstallJob(job, ctx) {
|
|
3540
|
+
export async function runSuiteInstallJob(job, ctx) {
|
|
3191
3541
|
const tmpDir = join(tmpdir(), `dsh-suite-${job.id}-${Date.now()}`)
|
|
3192
3542
|
const report = []
|
|
3193
3543
|
try {
|
|
@@ -3205,7 +3555,10 @@ async function runSuiteInstallJob(job, ctx) {
|
|
|
3205
3555
|
const patchPath = findPatchPath(ctx)
|
|
3206
3556
|
const profileDir = dirname(patchPath)
|
|
3207
3557
|
// 手动拉取子模块(git 的 insteadOf 重写对 submodule 不生效,按镜像 URL 逐个 clone)
|
|
3208
|
-
|
|
3558
|
+
// 套装级进度:套装动辄几分钟(clone 每个子模块 + 逐个装配),面板要能显示"第 i/n 个子模块"
|
|
3559
|
+
for (let si = 0; si < subs.length; si += 1) {
|
|
3560
|
+
const sub = subs[si]
|
|
3561
|
+
job.suiteProgress = { phase: 'clone', index: si + 1, total: subs.length, name: sub.name, done: false }
|
|
3209
3562
|
const subRepo = sub.url.replace(/^https?:\/\/[^/]+\//u, '').replace(/\.git$/u, '')
|
|
3210
3563
|
const dest = join(tmpDir, sub.path)
|
|
3211
3564
|
try {
|
|
@@ -3215,7 +3568,9 @@ async function runSuiteInstallJob(job, ctx) {
|
|
|
3215
3568
|
report.push({ component: sub.name, type: 'clone', ok: false, note: `子模块拉取失败:${error.message}` })
|
|
3216
3569
|
}
|
|
3217
3570
|
}
|
|
3218
|
-
for (
|
|
3571
|
+
for (let pi = 0; pi < subs.length; pi += 1) {
|
|
3572
|
+
const sub = subs[pi]
|
|
3573
|
+
job.suiteProgress = { phase: 'assemble', index: pi + 1, total: subs.length, name: sub.name, done: false }
|
|
3219
3574
|
const subDir = join(tmpDir, sub.path)
|
|
3220
3575
|
if (!existsSync(subDir)) continue
|
|
3221
3576
|
const subRepo = sub.url.replace(/^https?:\/\/[^/]+\//u, '').replace(/\.git$/u, '')
|
|
@@ -3271,7 +3626,12 @@ async function runSuiteInstallJob(job, ctx) {
|
|
|
3271
3626
|
// d. 普通 npm 插件(无 bundle 且无预设/技能)
|
|
3272
3627
|
if (!handled && pkg && typeof pkg.name === 'string') {
|
|
3273
3628
|
const target = join(profileDir, 'node_modules', pkg.name)
|
|
3274
|
-
if (existsSync(target))
|
|
3629
|
+
if (existsSync(target)) {
|
|
3630
|
+
// 核实清理结果:删不掉就明确报错,别把新包**合并**进旧目录(半新半旧最难查),
|
|
3631
|
+
// 也别让后续 rename/copyTree 以 EPERM 之类的次生错误掩盖真实原因(见 removeDirVerified)。
|
|
3632
|
+
const cleared = removeDirVerified(target)
|
|
3633
|
+
if (!cleared.ok) throw new Error(`无法清理已存在的目录(当前环境禁止删除):${target}${cleared.error ? `,原因:${cleared.error}` : ''}——请手动删除后重试`)
|
|
3634
|
+
}
|
|
3275
3635
|
mkdirSync(dirname(target), { recursive: true })
|
|
3276
3636
|
copyTree(subDir, target)
|
|
3277
3637
|
const taken = new Set(listEntries(ctx).map((e) => e.rowId))
|
|
@@ -3287,6 +3647,8 @@ async function runSuiteInstallJob(job, ctx) {
|
|
|
3287
3647
|
job.status = 'done'
|
|
3288
3648
|
job.stage = 'done'
|
|
3289
3649
|
job.suiteReport = report
|
|
3650
|
+
// 装配全部走完:进度标记完成(保留最后一轮的 i/n 与名字,前端显示"已完成 n/n 个子模块")
|
|
3651
|
+
if (job.suiteProgress) job.suiteProgress = { ...job.suiteProgress, done: true }
|
|
3290
3652
|
const okCount = report.filter((r) => r.ok).length
|
|
3291
3653
|
const failCount = report.filter((r) => !r.ok).length
|
|
3292
3654
|
const bundleCount = report.filter((r) => r.type === 'bundle' && r.ok).length
|
|
@@ -3473,9 +3835,11 @@ async function fetchSubpackageNames(repo, branch, auth) {
|
|
|
3473
3835
|
}
|
|
3474
3836
|
}
|
|
3475
3837
|
|
|
3476
|
-
/** 子包候选:聚合包(名字带 all)优先,上限 8 个。
|
|
3838
|
+
/** 子包候选:聚合包(名字带 all)优先,上限 8 个。
|
|
3839
|
+
* 2026-09-20 真装演练:`@dsh-suite/all` 这种 **scope 根形式**(`/all` 结尾)不被 `(^|-)all$` 命中,
|
|
3840
|
+
* 聚合包没能排到最前(那次纯属仓库目录顺序碰巧第一)。补上 `/all$`。 */
|
|
3477
3841
|
async function subpackageCandidates(repo, branch, auth) {
|
|
3478
|
-
const isAll = (name) => /(^|-)all$/u.test(name) || /-all-/u.test(name)
|
|
3842
|
+
const isAll = (name) => /(^|-)all$/u.test(name) || /-all-/u.test(name) || /\/all$/u.test(name)
|
|
3479
3843
|
const subs = await fetchSubpackageNames(repo, branch, auth)
|
|
3480
3844
|
return subs
|
|
3481
3845
|
.slice()
|
|
@@ -4888,7 +5252,7 @@ async function runInstallJob(job, ctx) {
|
|
|
4888
5252
|
job.status = 'failed'
|
|
4889
5253
|
// 文案要点:说清"这是读不到、不是没有",并给出可直接复制的安装命令 ——
|
|
4890
5254
|
// 用户看到"未发现子包"会去翻仓库找,而真相多半只是本次网络没读成。
|
|
4891
|
-
job.error = `仓库 ${job.repo} 的根包未发布到 npm(private: true),且本次没能读到它的子包列表(多为网络受限/超时,不代表没有子包)。请重试;或在「查看」详情里确认子包名后直接安装,例如:dsh plugin --profile web add
|
|
5255
|
+
job.error = `仓库 ${job.repo} 的根包未发布到 npm(private: true:${pkg.name}),且本次没能读到它的子包列表(多为网络受限/超时,不代表没有子包)。请重试;或在「查看」详情里确认子包名后直接安装,例如:dsh plugin --profile web add <子包名>;若该仓库根包本身即可作为插件,也可直接:dsh plugin --profile web add github:${job.repo}`
|
|
4892
5256
|
job.probeReason = 'subpackages-unreadable'
|
|
4893
5257
|
return
|
|
4894
5258
|
}
|
|
@@ -4924,9 +5288,16 @@ async function runInstallJob(job, ctx) {
|
|
|
4924
5288
|
// 可配置软件源:主→备依次尝试(默认 npmmirror → npmjs,可增删自定义/内网源)
|
|
4925
5289
|
const registries = orderedRegistries(readSources())
|
|
4926
5290
|
const deadline = Date.now() + 8 * 60 * 1000
|
|
5291
|
+
// 子包级进度(2026-09-20 真装实测:11 个子包的聚合仓库跑了 19 分钟,job.stage 一直停在
|
|
5292
|
+
// installing,面板只有一个不动的进度条)。candidateTotal/Index/Name 每轮开始时更新,
|
|
5293
|
+
// 由 installJobView 折算成 progress{index,total,name} 下发给面板。
|
|
5294
|
+
job.candidateTotal = candidates.length
|
|
5295
|
+
job.candidateDone = false
|
|
4927
5296
|
for (let index = 0; index < candidates.length && installedName === null; index += 1) {
|
|
4928
5297
|
if (Date.now() > deadline) break
|
|
4929
5298
|
const name = candidates[index]
|
|
5299
|
+
job.candidateIndex = index + 1
|
|
5300
|
+
job.candidateName = name
|
|
4930
5301
|
// 加法优化:包已在 node_modules 且名字匹配时,不再重复安装/触发 EPERM,直接进入启用流程
|
|
4931
5302
|
if (installedName === null) {
|
|
4932
5303
|
const existingTarget = join(profileDir, 'node_modules', name, 'package.json')
|
|
@@ -5049,12 +5420,15 @@ async function runInstallJob(job, ctx) {
|
|
|
5049
5420
|
if (subs.length > 0) {
|
|
5050
5421
|
const extra = subs
|
|
5051
5422
|
.slice()
|
|
5052
|
-
|
|
5423
|
+
// 聚合包优先:`@dsh-suite/all` 这类 scope 根形式(/all 结尾)也要认(演练实测)
|
|
5424
|
+
.sort((a, b) => Number(/(^|-)all$/u.test(b.name) || /-all-/u.test(b.name) || /\/all$/u.test(b.name)) - Number(/(^|-)all$/u.test(a.name) || /-all-/u.test(a.name) || /\/all$/u.test(a.name)))
|
|
5053
5425
|
.map((sub) => sub.name)
|
|
5054
5426
|
.filter((n) => !candidates.includes(n))
|
|
5055
5427
|
.slice(0, 8)
|
|
5056
5428
|
if (extra.length > 0) {
|
|
5057
5429
|
candidates = [...candidates, ...extra]
|
|
5430
|
+
// 懒惰展开后候选变多:总数要跟着更新,否则面板会显示"第 9/1 个"
|
|
5431
|
+
job.candidateTotal = candidates.length
|
|
5058
5432
|
if (!Array.isArray(job.subpackages)) job.subpackages = []
|
|
5059
5433
|
for (const e of extra) if (!job.subpackages.includes(e)) job.subpackages.push(e)
|
|
5060
5434
|
}
|
|
@@ -5065,10 +5439,14 @@ async function runInstallJob(job, ctx) {
|
|
|
5065
5439
|
// 本地 AI 兜底会调用模型 API、产生费用:挂起等待用户明确同意后再执行
|
|
5066
5440
|
job.stage = 'ai-consent'
|
|
5067
5441
|
job.aiPending = { lastError: lastError?.message ?? null }
|
|
5442
|
+
// 等授权期间要能展示"为什么卡住、还能等多久":请求时间 + 超时上限 + 最后一个确定性错误
|
|
5443
|
+
job.aiPendingSince = Date.now()
|
|
5444
|
+
job.aiConsentTimeoutMs = AI_CONSENT_TIMEOUT_MS
|
|
5445
|
+
job.lastError = lastError?.message ?? null
|
|
5068
5446
|
job.aiWait = new Promise((resolve) => { job.aiPending.resolver = resolve })
|
|
5069
5447
|
const decision = await Promise.race([
|
|
5070
5448
|
job.aiWait,
|
|
5071
|
-
new Promise((resolve) => setTimeout(() => resolve({ approved: false, timeout: true }),
|
|
5449
|
+
new Promise((resolve) => setTimeout(() => resolve({ approved: false, timeout: true }), AI_CONSENT_TIMEOUT_MS)),
|
|
5072
5450
|
])
|
|
5073
5451
|
job.aiPending = null
|
|
5074
5452
|
job.aiWait = null
|
|
@@ -5076,12 +5454,17 @@ async function runInstallJob(job, ctx) {
|
|
|
5076
5454
|
await aiRepair(job, ctx, profileDir, candidates, lastError?.message ?? null)
|
|
5077
5455
|
} else {
|
|
5078
5456
|
job.status = 'failed'
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5457
|
+
// 失败/取消时清场并如实汇报(见 cleanupAttemptedCandidates 注释):
|
|
5458
|
+
// 不能让用户面对"面板说失败、磁盘上却留了半个包和 _tmp_ 残留"的糊涂账。
|
|
5459
|
+
const leftovers = cleanupAttemptedCandidates(profileDir, candidates)
|
|
5460
|
+
job.leftovers = leftovers
|
|
5461
|
+
job.error = aiConsentFailureText(decision, leftovers)
|
|
5082
5462
|
}
|
|
5083
5463
|
return
|
|
5084
5464
|
}
|
|
5465
|
+
// 装成功:进度标记完成(index 停在真正装上的那一轮,前端显示"第 i/n 个:<包名>"即为成功项)
|
|
5466
|
+
job.candidateDone = true
|
|
5467
|
+
job.candidateName = installedName
|
|
5085
5468
|
job.packageName = installedName
|
|
5086
5469
|
// 聚合包子包版本对齐(确保更新后所有子包也到新版声明版本)
|
|
5087
5470
|
let syncedNames = []
|
|
@@ -5284,6 +5667,10 @@ async function handle(ctx, req, res) {
|
|
|
5284
5667
|
const auth = readGithubAuth()
|
|
5285
5668
|
const jobs = [...installJobs.values()].filter((job) => job.status === 'installing').map(installJobView)
|
|
5286
5669
|
const recentFailures = [...installJobs.values()].filter((job) => job.status === 'failed').slice(-3).map(installJobView)
|
|
5670
|
+
// 已安装但尚未生效的任务(2026-09-20 真装真卸演练实测):bundle 型插件要重启才被加载,装完
|
|
5671
|
+
// /install 返回 entryId: null、/state 里新增 loader 条目 = 0 —— 面板过去完全看不出「装了但还没
|
|
5672
|
+
// 生效」,也无法撤销。前端据此渲染「已安装·重启后生效」徽标 + 按 jobId 删除。
|
|
5673
|
+
const pendingRestart = pendingRestartJobs([...installJobs.values()], listEntries(ctx))
|
|
5287
5674
|
// 框架升级检测与适配(备份快照 + 重打框架补丁),try 包裹不阻塞 state 返回
|
|
5288
5675
|
let framework = null
|
|
5289
5676
|
try { framework = detectFrameworkUpgrade(ctx) } catch {}
|
|
@@ -5298,7 +5685,7 @@ async function handle(ctx, req, res) {
|
|
|
5298
5685
|
const selfPkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'))
|
|
5299
5686
|
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
5300
5687
|
} catch {}
|
|
5301
|
-
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0 }, compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null })) }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
5688
|
+
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0 }, compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null })) }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
5302
5689
|
return
|
|
5303
5690
|
}
|
|
5304
5691
|
|
|
@@ -5713,13 +6100,22 @@ async function handle(ctx, req, res) {
|
|
|
5713
6100
|
}
|
|
5714
6101
|
|
|
5715
6102
|
if (pathname === `${ROUTE_PREFIX}/uninstall`) {
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
6103
|
+
// 入参两种形态:entryId(运行中的 loader 条目)· jobId(已安装但尚未生效的安装任务,见 revokePendingInstall)
|
|
6104
|
+
const { entryId, jobId } = body
|
|
6105
|
+
const entryIdOk = typeof entryId === 'string' && /^[A-Za-z0-9_:.-]{1,80}$/u.test(entryId)
|
|
6106
|
+
const jobIdOk = typeof jobId === 'string' && /^[A-Za-z0-9_.:-]{1,80}$/u.test(jobId)
|
|
6107
|
+
if (!entryIdOk && !jobIdOk) {
|
|
6108
|
+
sendError(res, 400, 'entryId 无效(撤销尚未生效的安装请改传 jobId)')
|
|
5719
6109
|
return
|
|
5720
6110
|
}
|
|
5721
|
-
const entry = ctx.loader.entries().find((candidate) => candidate.id === entryId)
|
|
6111
|
+
const entry = entryIdOk ? ctx.loader.entries().find((candidate) => candidate.id === entryId) : undefined
|
|
5722
6112
|
if (!entry) {
|
|
6113
|
+
// 2026-09-20 演练实测缺口:bundle 型插件装完要重启才进 loader,过去这里恒 404 →
|
|
6114
|
+
// 「刚装错的插件在重启前无法从面板卸载」。带 jobId 时改走安装任务撤销。
|
|
6115
|
+
if (jobIdOk) {
|
|
6116
|
+
await uninstallByJobId(ctx, res, jobId)
|
|
6117
|
+
return
|
|
6118
|
+
}
|
|
5723
6119
|
sendError(res, 404, `没有名为 ${entryId} 的插件条目`)
|
|
5724
6120
|
return
|
|
5725
6121
|
}
|
|
@@ -5760,11 +6156,17 @@ async function handle(ctx, req, res) {
|
|
|
5760
6156
|
}
|
|
5761
6157
|
await removeInsertRow(patchPath, rowId)
|
|
5762
6158
|
let uninstallError = null
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
6159
|
+
// 目录已不在、manifest 也没引用 → 不必再拉一次 pnpm:那只会在本机换来一句没有信息量的
|
|
6160
|
+
// "Command failed: corepack pnpm remove …"(演练实测)。判断规则与 jobId 撤销分支同一套。
|
|
6161
|
+
if (await needsPnpmRemove(profileDir, moduleName)) {
|
|
6162
|
+
try {
|
|
6163
|
+
await pnpmRemove(profileDir, moduleName)
|
|
6164
|
+
} catch (error) {
|
|
6165
|
+
uninstallError = error instanceof Error ? error.message : String(error)
|
|
6166
|
+
}
|
|
5767
6167
|
}
|
|
6168
|
+
// 任务记录是否作废看**事实**(包目录真的没了),不看 pnpm 的退出码;删不干净时留着让用户重试
|
|
6169
|
+
if (!existsSync(packageDirIn(profileDir, moduleName))) markJobsRevoked(moduleName, rowId)
|
|
5768
6170
|
sendJson(res, 200, { ok: true, removed: 'entry', packageName: moduleName, restart: false, uninstallError })
|
|
5769
6171
|
return
|
|
5770
6172
|
}
|
|
@@ -7514,6 +7916,7 @@ async function handle(ctx, req, res) {
|
|
|
7514
7916
|
const profileDir = dirname(findPatchPath(ctx))
|
|
7515
7917
|
const nodeModules = join(profileDir, 'node_modules')
|
|
7516
7918
|
const removed = []
|
|
7919
|
+
const failed = []
|
|
7517
7920
|
// 1) 删除 .old-* 残余备份(顶层 + 作用域目录)
|
|
7518
7921
|
const scanDirs = [nodeModules]
|
|
7519
7922
|
try { if (existsSync(join(nodeModules, '@linxin666'))) scanDirs.push(join(nodeModules, '@linxin666')) } catch {}
|
|
@@ -7523,8 +7926,10 @@ async function handle(ctx, req, res) {
|
|
|
7523
7926
|
if (!entry.isDirectory()) continue
|
|
7524
7927
|
if (!/\.old-/u.test(entry.name)) continue
|
|
7525
7928
|
const target = join(dir, entry.name)
|
|
7526
|
-
|
|
7527
|
-
|
|
7929
|
+
// 核实后再计入 removed:删不掉却报成功会让用户以为清干净了(见 removeDirVerified 注释)
|
|
7930
|
+
const result = removeDirVerified(target)
|
|
7931
|
+
if (result.ok) removed.push(entry.name)
|
|
7932
|
+
else failed.push({ name: entry.name, path: target, error: result.error })
|
|
7528
7933
|
}
|
|
7529
7934
|
} catch {}
|
|
7530
7935
|
}
|
|
@@ -7542,12 +7947,18 @@ async function handle(ctx, req, res) {
|
|
|
7542
7947
|
if (declared.has(full)) continue
|
|
7543
7948
|
const pkgJson = join(scoped, entry.name, 'package.json')
|
|
7544
7949
|
if (existsSync(pkgJson)) {
|
|
7545
|
-
|
|
7546
|
-
|
|
7950
|
+
const target = join(scoped, entry.name)
|
|
7951
|
+
const result = removeDirVerified(target)
|
|
7952
|
+
if (result.ok) removed.push(full)
|
|
7953
|
+
else failed.push({ name: full, path: target, error: result.error })
|
|
7547
7954
|
}
|
|
7548
7955
|
}
|
|
7549
7956
|
}
|
|
7550
7957
|
} catch {}
|
|
7958
|
+
if (failed.length > 0) {
|
|
7959
|
+
sendJson(res, 200, { ok: false, removed, count: removed.length, failed, error: `有 ${failed.length} 项没能删除(目录仍存在):${failed.map((f) => f.name).join('、')}——当前环境可能禁止删除,请手动处理` })
|
|
7960
|
+
return
|
|
7961
|
+
}
|
|
7551
7962
|
sendJson(res, 200, { ok: true, removed, count: removed.length })
|
|
7552
7963
|
return
|
|
7553
7964
|
}
|
|
@@ -7675,12 +8086,14 @@ async function handle(ctx, req, res) {
|
|
|
7675
8086
|
sendError(res, 404, `技能 ${name} 不存在`)
|
|
7676
8087
|
return
|
|
7677
8088
|
}
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
sendError(res, 500,
|
|
8089
|
+
// 删完必须核实:本机环境可能让 rmSync 静默落空(见 removeDirVerified 注释),
|
|
8090
|
+
// 旧代码删完直接 {ok:true} → 用户以为删了,技能其实还在(2026-09-20 演练实测)。
|
|
8091
|
+
const result = removeDirVerified(dest)
|
|
8092
|
+
if (!result.ok) {
|
|
8093
|
+
sendError(res, 500, `删除技能失败:目录仍存在(${dest})${result.error ? `,原因:${result.error}` : ''}——当前环境可能禁止删除该目录,请手动删除它`)
|
|
8094
|
+
return
|
|
7683
8095
|
}
|
|
8096
|
+
sendJson(res, 200, { ok: true, name })
|
|
7684
8097
|
return
|
|
7685
8098
|
}
|
|
7686
8099
|
|
|
@@ -7909,7 +8322,9 @@ async function handle(ctx, req, res) {
|
|
|
7909
8322
|
} catch (error) {
|
|
7910
8323
|
lastError = error
|
|
7911
8324
|
// 失败可能留下半成品目录,清理后再试下一个源,否则会因目标已存在而连环失败
|
|
7912
|
-
|
|
8325
|
+
// 失败可能留下半成品目录,清理后再试下一个源,否则会因目标已存在而连环失败;
|
|
8326
|
+
// 清理同样要核实(见 removeDirVerified)——清不掉时下一个源只会报"目录非空"。
|
|
8327
|
+
removeDirVerified(target)
|
|
7913
8328
|
}
|
|
7914
8329
|
}
|
|
7915
8330
|
if (!cloned) throw lastError ?? new Error('未知错误')
|
|
@@ -7952,12 +8367,14 @@ async function handle(ctx, req, res) {
|
|
|
7952
8367
|
sendError(res, 400, '路径不在仓库落地目录内或不存在')
|
|
7953
8368
|
return
|
|
7954
8369
|
}
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
sendError(res, 500,
|
|
8370
|
+
// 核实删除结果:本机环境可能让 rmSync 静默落空(见 removeDirVerified 注释),
|
|
8371
|
+
// 删不掉却回 {ok:true} 会让用户以为仓库已清理。
|
|
8372
|
+
const result = removeDirVerified(target)
|
|
8373
|
+
if (!result.ok) {
|
|
8374
|
+
sendError(res, 500, `删除失败:目录仍存在(${target})${result.error ? `,原因:${result.error}` : ''}——当前环境可能禁止删除该目录,请手动删除`)
|
|
8375
|
+
return
|
|
7960
8376
|
}
|
|
8377
|
+
sendJson(res, 200, { ok: true })
|
|
7961
8378
|
return
|
|
7962
8379
|
}
|
|
7963
8380
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.54",
|
|
4
4
|
"description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|