@noob-stupid/dsh-plugin-console 0.5.30 → 0.5.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@
66
66
  dsh plugin --profile web add @noob-stupid/dsh-plugin-console
67
67
 
68
68
  # or install from GitHub source (needs git; allowBuilds authorization on first add)
69
- dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
69
+ dsh plugin --profile web add github:Noob-stupid/dsh-plugin-gating-hub
70
70
  ```
71
71
 
72
72
  Then restart the dsh service → refresh the page → **Settings → Plugins → Plugin Console**.
@@ -78,13 +78,13 @@ Then restart the dsh service → refresh the page → **Settings → Plugins →
78
78
  Windows (PowerShell):
79
79
 
80
80
  ```powershell
81
- git clone https://github.com/Noob-stupid/dsh-plugin-hub "$env:TEMP\dsh-plugin-console" 2>$null; & "$env:TEMP\dsh-plugin-console\deploy.ps1"
81
+ git clone https://github.com/Noob-stupid/dsh-plugin-gating-hub "$env:TEMP\dsh-plugin-console" 2>$null; & "$env:TEMP\dsh-plugin-console\deploy.ps1"
82
82
  ```
83
83
 
84
84
  Linux / macOS:
85
85
 
86
86
  ```bash
87
- git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console 2>/dev/null; bash /tmp/dsh-plugin-console/deploy.sh
87
+ git clone https://github.com/Noob-stupid/dsh-plugin-gating-hub /tmp/dsh-plugin-console 2>/dev/null; bash /tmp/dsh-plugin-console/deploy.sh
88
88
  ```
89
89
 
90
90
  The script copies the plugin into `$DSH_HOME/profiles/<profile>/node_modules/` and
@@ -96,7 +96,7 @@ idempotently appends an enable entry to `cordis.patch.yml`. Afterwards:
96
96
 
97
97
  ### Option 3: hand it to an AI in one sentence
98
98
 
99
- > Install the DSH plugin hub (dsh-plugin-hub): run `dsh plugin --profile web add @noob-stupid/dsh-plugin-console` (npm release); if there is no dsh CLI, clone https://github.com/Noob-stupid/dsh-plugin-hub to `~/.dsh/profiles/web/node_modules/` and register it in `cordis.patch.yml` (id: plugin-console, name: @noob-stupid/dsh-plugin-console). Restart dsh web afterwards.
99
+ > Install the DSH plugin hub (dsh-plugin-hub): run `dsh plugin --profile web add @noob-stupid/dsh-plugin-console` (npm release); if there is no dsh CLI, clone https://github.com/Noob-stupid/dsh-plugin-gating-hub to `~/.dsh/profiles/web/node_modules/` and register it in `cordis.patch.yml` (id: plugin-console, name: @noob-stupid/dsh-plugin-console). Restart dsh web afterwards.
100
100
 
101
101
  Requires: DSH ≥ 0.1.0-rc.6 (web profile, with `dsh-client-modules` / `dsh-host-plugin-inventory`).
102
102
 
@@ -384,7 +384,7 @@ If this panel saves you time or makes DSH more fun to use:
384
384
  - ⭐ **Star this repo** — it directly helps more DSH users find it;
385
385
  - Share it with DSH users or in DSH communities;
386
386
  - Submit your own plugin (tag it `dsh-plugin`) to grow the ecosystem;
387
- - Found a bug or want a feature? [Open an issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues).
387
+ - Found a bug or want a feature? [Open an issue](https://github.com/Noob-stupid/dsh-plugin-gating-hub/issues).
388
388
 
389
389
  ---
390
390
 
@@ -395,7 +395,7 @@ Contributions of all kinds are welcome — issues, PRs, docs, translations.
395
395
  - **Guidelines**: [CONTRIBUTING.md](CONTRIBUTING.md)
396
396
  - **Code of Conduct**: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
397
397
  - **Security policy**: [SECURITY.md](SECURITY.md) (report vulnerabilities privately)
398
- - **Issue templates**: bug reports & feature requests via the [new issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues/new/choose) page
398
+ - **Issue templates**: bug reports & feature requests via the [new issue](https://github.com/Noob-stupid/dsh-plugin-gating-hub/issues/new/choose) page
399
399
 
400
400
  ---
401
401
 
@@ -420,4 +420,4 @@ The layered-refactor preview lives in its **own repository**: [**`Noob-stupid/ds
420
420
  - **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.57`**, maintainability only;
421
421
  - **Verified**: 19 test suites green · 8 architecture-guard assertions · **route inventory identical** (`status` + response fields) against the stable build;
422
422
  - **Not yet exercised**: real framework upgrade / rollback, restart guardian, component start/stop, real AI run, Gitee OAuth (~10–25% long-tail risk, subjective);
423
- - **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.67`, or `main` of this repo.
423
+ - **For everyday use** stick to the **latest npm release** of `@noob-stupid/dsh-plugin-console` (`npm view @noob-stupid/dsh-plugin-console version`), or `main` of this repo.
package/README.zh.md CHANGED
@@ -45,7 +45,7 @@
45
45
  dsh plugin --profile web add @noob-stupid/dsh-plugin-console
46
46
 
47
47
  # 或直接装 GitHub 源码(需 git,首次需 allowBuilds 授权)
48
- dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
48
+ dsh plugin --profile web add github:Noob-stupid/dsh-plugin-gating-hub
49
49
  ```
50
50
 
51
51
  然后重启 dsh 服务 → 刷新页面 → **设置 → 插件 → 插件控制台**。
@@ -57,13 +57,13 @@ dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
57
57
  Windows(PowerShell):
58
58
 
59
59
  ```powershell
60
- git clone https://github.com/Noob-stupid/dsh-plugin-hub "$env:TEMP\dsh-plugin-console" 2>$null; & "$env:TEMP\dsh-plugin-console\deploy.ps1"
60
+ git clone https://github.com/Noob-stupid/dsh-plugin-gating-hub "$env:TEMP\dsh-plugin-console" 2>$null; & "$env:TEMP\dsh-plugin-console\deploy.ps1"
61
61
  ```
62
62
 
63
63
  Linux / macOS:
64
64
 
65
65
  ```bash
66
- git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console 2>/dev/null; bash /tmp/dsh-plugin-console/deploy.sh
66
+ git clone https://github.com/Noob-stupid/dsh-plugin-gating-hub /tmp/dsh-plugin-console 2>/dev/null; bash /tmp/dsh-plugin-console/deploy.sh
67
67
  ```
68
68
 
69
69
  脚本会做两件事:把插件包拷进 `$DSH_HOME/profiles/<profile>/node_modules/`,
@@ -74,7 +74,7 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
74
74
 
75
75
  ### 方式三:一句话交给 AI
76
76
 
77
- > 安装 DSH 插件中心(dsh-plugin-hub):运行 `dsh plugin --profile web add @noob-stupid/dsh-plugin-console`(npm 发布版);若没有 dsh CLI,则克隆 https://github.com/Noob-stupid/dsh-plugin-hub 到 `~/.dsh/profiles/web/node_modules/`,在 `cordis.patch.yml` 注册(id: plugin-console,name: @noob-stupid/dsh-plugin-console)。完成后重启 dsh web。
77
+ > 安装 DSH 插件中心(dsh-plugin-hub):运行 `dsh plugin --profile web add @noob-stupid/dsh-plugin-console`(npm 发布版);若没有 dsh CLI,则克隆 https://github.com/Noob-stupid/dsh-plugin-gating-hub 到 `~/.dsh/profiles/web/node_modules/`,在 `cordis.patch.yml` 注册(id: plugin-console,name: @noob-stupid/dsh-plugin-console)。完成后重启 dsh web。
78
78
 
79
79
  要求:DSH ≥ 0.1.0-rc.6(web profile,含 `dsh-client-modules` / `dsh-host-plugin-inventory`)。
80
80
 
@@ -287,7 +287,7 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
287
287
 
288
288
  ## 帮助 / Help
289
289
 
290
- 遇到问题先看这里;仍有疑问请到 [议题](https://github.com/Noob-stupid/dsh-plugin-hub/issues) 提问。
290
+ 遇到问题先看这里;仍有疑问请到 [议题](https://github.com/Noob-stupid/dsh-plugin-gating-hub/issues) 提问。
291
291
 
292
292
  - **面板没出现**:重启 dsh 服务 → 刷新页面 → 设置 → 插件 → 插件管理。
293
293
  - **点开关没反应**:基础设施行带"受保护"标签(禁止开关,这是保护机制);普通插件开关经
@@ -318,7 +318,7 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
318
318
  - ⭐ **给本仓库点个 Star**——直接帮助更多 DSH 用户发现它;
319
319
  - 分享给身边的 DSH 用户或社区;
320
320
  - 提交你自己的插件(打上 `dsh-plugin` 标签)一起壮大生态;
321
- - 发现 bug 或有新需求?[开一个 Issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues)。
321
+ - 发现 bug 或有新需求?[开一个 Issue](https://github.com/Noob-stupid/dsh-plugin-gating-hub/issues)。
322
322
 
323
323
  ---
324
324
 
@@ -329,7 +329,7 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
329
329
  - **贡献指南**:[CONTRIBUTING.md](CONTRIBUTING.md)
330
330
  - **行为准则**:[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
331
331
  - **安全政策**:[SECURITY.md](SECURITY.md)(漏洞私下报告)
332
- - **Issue 模板**:从 [新建 Issue](https://github.com/Noob-stupid/dsh-plugin-hub/issues/new/choose) 页面选择 Bug / 功能建议
332
+ - **Issue 模板**:从 [新建 Issue](https://github.com/Noob-stupid/dsh-plugin-gating-hub/issues/new/choose) 页面选择 Bug / 功能建议
333
333
 
334
334
  ---
335
335
 
@@ -355,5 +355,5 @@ MIT
355
355
  - **已验证**:19 套测试全绿 · 8 条架构守卫断言 · 与稳定版逐条对打**路由清单一致**(`status` + 响应字段);
356
356
  - **已实测**(2026-09-20 真装真卸演练):普通插件 / bundle 插件 / 无 npm 仓库 / 套装 / 技能 / 聚合仓库子包 / 仓库落地 / 服务器组件启停;
357
357
  - **尚未实测**:真框架升级 / 真回滚、重启守护链路、AI 真跑、Gitee OAuth 回调(长尾风险主观估计 **10%~25%**);
358
- - **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.67`,或本仓库 `main`。
358
+ - **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console` 的**最新发布版**(`npm view @noob-stupid/dsh-plugin-console version`),或本仓库 `main`。
359
359
  - 请多反馈问题
package/SECURITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 安全政策 Security Policy
2
2
 
3
- 本文件说明 `@noob-stupid/dsh-plugin-console`(仓库 [Noob-stupid/dsh-plugin-hub](https://github.com/Noob-stupid/dsh-plugin-hub))的安全政策:这个插件**能做什么**(能力清单)、已内置哪些护栏、自动化静态扫描容易误报的地方,以及如何报告漏洞。
3
+ 本文件说明 `@noob-stupid/dsh-plugin-console`(仓库 [Noob-stupid/dsh-plugin-gating-hub](https://github.com/Noob-stupid/dsh-plugin-gating-hub))的安全政策:这个插件**能做什么**(能力清单)、已内置哪些护栏、自动化静态扫描容易误报的地方,以及如何报告漏洞。
4
4
 
5
5
  英文能力摘要见文末 [Capability summary (English)](#capability-summary-english)。
6
6
 
@@ -15,7 +15,7 @@
15
15
 
16
16
  **请勿在公开 Issue 中披露安全漏洞**。请通过以下方式私下报告:
17
17
 
18
- 1. 通过 GitHub 的 [Security Advisory](https://github.com/Noob-stupid/dsh-plugin-hub/security/advisories/new)(推荐,私密)
18
+ 1. 通过 GitHub 的 [Security Advisory](https://github.com/Noob-stupid/dsh-plugin-gating-hub/security/advisories/new)(推荐,私密)
19
19
  2. 或给仓库 owner 发 GitHub 私信
20
20
 
21
21
  请在报告中包含:
package/lib/client.js CHANGED
@@ -500,7 +500,7 @@ window.__ModuleLoader__.load({
500
500
  archiveSourcesDesc: "git 协议拉不动时的替代通道:直接用 HTTP 下载仓库压缩包再解压建仓(真机实测同一镜像下 archive 4 MB/s、git 协议 0 B/s)。模板用 {owner}/{repo} 占位符,可选 {branch};主源优先,失败自动回退下一个。示例:https://ghproxy.net/https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.tar.gz",
501
501
  archiveUrlPlaceholder: "https://ghproxy.net/https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.tar.gz",
502
502
  addArchiveSource: "添加 archive 源",
503
- gitSourcesDesc: "克隆仓库时用的地址模板({owner}/{repo} 占位符)。下面这个列表就是「自定义 Git 源」——内置两条是默认值,点「添加 Git 源」即可换成你自己的:Gitee = https://gitee.com/{owner}/{repo}.git;GitLab = https://gitlab.com/{owner}/{repo}.git;自建 Gitea = https://你的域名/{owner}/{repo}.git;镜像代理 = https://镜像站/https://github.com/{owner}/{repo}.git;本地裸仓库 = file:///D:/repos/{owner}/{repo}.git。主源优先,失败自动回退下一个",
503
+ gitSourcesDesc: "克隆仓库时用的地址模板({owner}/{repo} 占位符)。下面这个列表就是「自定义 Git 源」——内置两条是默认值,点「添加 Git 源」即可换成你自己的:Gitee = https://gitee.com/{owner}/{repo}.git;GitLab = https://gitlab.com/{owner}/{repo}.git;自建 Gitea = https://你的域名/{owner}/{repo}.git;镜像代理 = https://镜像站/https://github.com/{owner}/{repo}.git;本地裸仓库 = file:///C:/repos/{owner}/{repo}.git。主源优先,失败自动回退下一个",
504
504
  addGitSource: "添加 Git 源",
505
505
  gitUrlPlaceholder: "如 https://gitee.com/{owner}/{repo}.git",
506
506
  gitSourceBtn: "Git 源",
@@ -1050,7 +1050,7 @@ window.__ModuleLoader__.load({
1050
1050
  archiveSourcesDesc: "Fallback transport when the git protocol cannot move data: download the repo tarball over plain HTTP, extract, and create a repo from it (measured: on the same mirror archive ran at 4 MB/s while git moved 0 B/s). Templates use {owner}/{repo}, optionally {branch}; primary first, falls back on failure.",
1051
1051
  archiveUrlPlaceholder: "https://ghproxy.net/https://github.com/{owner}/{repo}/archive/refs/heads/{branch}.tar.gz",
1052
1052
  addArchiveSource: "Add archive source",
1053
- gitSourcesDesc: "URL template for cloning ({owner}/{repo} placeholders). This list IS the custom git-source list — the two built-ins are just defaults; click Add git source to swap in your own: Gitee = https://gitee.com/{owner}/{repo}.git; GitLab = https://gitlab.com/{owner}/{repo}.git; self-hosted Gitea = https://your-domain/{owner}/{repo}.git; mirror proxy = https://mirror/https://github.com/{owner}/{repo}.git; local bare repo = file:///D:/repos/{owner}/{repo}.git. Primary first, falls back on failure",
1053
+ gitSourcesDesc: "URL template for cloning ({owner}/{repo} placeholders). This list IS the custom git-source list — the two built-ins are just defaults; click Add git source to swap in your own: Gitee = https://gitee.com/{owner}/{repo}.git; GitLab = https://gitlab.com/{owner}/{repo}.git; self-hosted Gitea = https://your-domain/{owner}/{repo}.git; mirror proxy = https://mirror/https://github.com/{owner}/{repo}.git; local bare repo = file:///C:/repos/{owner}/{repo}.git. Primary first, falls back on failure",
1054
1054
  addGitSource: "Add Git source",
1055
1055
  gitUrlPlaceholder: "e.g. https://gitee.com/{owner}/{repo}.git",
1056
1056
  gitSourceBtn: "Git sources",
package/lib/index.js CHANGED
@@ -1,141 +1,141 @@
1
- /**
2
- * @deepseek-ai/dsh-plugin-console — 插件控制台宿主端。
3
- *
4
- * 提供环回 HTTP 路由(前缀 /plugin-console):
5
- * GET /plugin-console/state 当前插件清单 + 用户补丁层状态
6
- * POST /plugin-console/toggle 一键启用/停用插件(写 cordis.patch.yml,HMR 生效)
7
- * POST /plugin-console/search GitHub 仓库搜索(dsh-plugin 相关)
8
- * POST /plugin-console/repo 读取仓库的 package.json,判断是否可安装
9
- * POST /plugin-console/install 安装 npm 包(或 git 仓库)并追加启用条目
10
- *
11
- * 插件开关的机制:用户补丁层 cordis.patch.yml 是逐键覆盖(id-targeted patch),
12
- * 追加 `- id: X` + `disabled: true` 即可停用任意行(含 bundle 行与用户 insert 行),
13
- * 移除该块即恢复;HMR 监视器会自动重组合,无需重启。
14
- */
15
- import { readFile, writeFile } from 'node:fs/promises'
16
- import { readFileSync, writeFileSync, existsSync, statSync, rmSync, readdirSync, mkdirSync, copyFileSync, realpathSync } from 'node:fs'
17
- import { execFile, spawn } from 'node:child_process'
18
- import { promisify } from 'node:util'
19
- import { dirname, join, basename, sep } from 'node:path'
20
- import { tmpdir, homedir } from 'node:os'
21
- import { fileURLToPath, pathToFileURL } from 'node:url'
22
- import { createRequire } from 'node:module'
23
- import { request as httpsRequest } from 'node:https'
24
- import { aiJobsFile, componentsFile, dshHome, entryPkgMeta, findPatchPath, marketIndexCacheFile, packageNameOf, pluginRoot, profileDirOf, repoLandConfFile, resolvePackageJson, rowIdOf } from './server/infra/paths.js'
25
- import { isFrameworkVersionNewer, parseFrameworkVersion, semverRangeMatch, semverRangeMatchLoose } from './server/infra/semver.js'
26
- import { GITHUB_API, curlJson, fetchJsonUrl, githubJson, postJsonUrl, rawTextWithFallback } from './server/infra/http.js'
27
- import { cleanupStalePackageDir, copyTree, queuedWrite, startTrashCleanup, trashScanRoots } from './server/infra/fsx.js'
28
- import { execFileAsync, gitEnv, processAlive } from './server/infra/exec.js'
29
- import { escapeRegExp, maskUrl } from './server/infra/mask.js'
30
- import { isAllowedWriteOrigin, isLoopback, readBody, sendError, sendJson } from './server/infra/httpd.js'
31
- import { DEFAULT_SOURCES, consumeGiteeOAuthState, createGiteeOAuthState, gitCloneUrls, giteeStatusView, isAllowedGitSourceUrl, isAllowedSourceUrl, maskSources, orderedRegistries, readGiteeConfig, readSources, writeSources } from './server/domain/sources.js'
32
- import { CORE_PATCH_ROW_IDS, appendInsert, disableEntry, enableEntry, healPatchSafety, readPatchState, removeDisableBlock, removeInsertRow, sanitizePatchText, syncNameFromNote } from './server/domain/patch.js'
33
- import { enrichItems, fetchRepoPackage, fetchSubpackageNames, githubRepoInfo, normalizePlatformItems, readPluginDetails, subpackageCandidates } from './server/domain/market.js'
34
- import { SKILL_TOPICS, detectSkillRepo, fetchSkillMeta, listInstalledSkills, runSkillInstallJob, setSkillEnabled } from './server/domain/skills.js'
35
- import { autostartComponents, compFind, compStart, compStatus, compStop, compUiUrl, compUpsert, findComponents } from './server/domain/components.js'
36
- import { getReposDir, gitCloneRepo, listLandedRepos, setReposDir } from './server/domain/repoland.js'
37
- import { DEFAULT_BUNDLES, addBundleToManifest, backfillMissingDeps, curlManualInstall, detectBundleOnly, ensureBundlePatchIntegrity, githubReleaseInstall, installJobView, pnpmInstall, raceInstallChannels, readBundlePatchRefNames, readExtraBundleOwners, readGithubAuth, syncAggregateSubpackageVersions } from './server/domain/install.js'
38
- import { findPresetDirs, readGitmodules } from './server/domain/suite.js'
39
- import { FIBER_PHASE, SUPPORTED_WEB_APP_PATTERN, checkPluginFrameworkCompat, frameworkCheckPromptText, isFrameworkOwnedPackage, probePluginImport, readCompatGate, readCompatPending, writeCompatGate, writeCompatPending } from './server/domain/compat.js'
40
- import { logQuarantineMergeError, markPendingAdopted, mergeQuarantineRecord, planQuarantine, readQuarantineRecord, reconcileCompatPending } from './server/domain/quarantine.js'
41
- import { FRAMEWORK_BACKUP_ROOT, FRAMEWORK_STATE_FILE, applyFrameworkTolerancePatchOnce, checkpointFrameworkTree, cleanupStaleFwTasks, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules } from './server/domain/framework.js'
42
- import { migrateAgentConfigsForUpgrade } from './server/domain/presets.js'
43
- import { GITEE_AUTH_URL, GITEE_TOKEN_URL } from './server/domain/sources.js'
44
- import { aiEmpowerPresetFor, aiJobs, builtinPlanFor, isAllowedWritePath, isSafeRunCmd, loadAiJobs, parsePlanJson, resolvePlaceholders, resolvePythonPath, saveAiJobs } from './server/domain/ai.js'
45
- import { jobLog, waitHealth } from './server/domain/jobs.js'
46
- import { fwCheckCache, installJobs, marketIndexCache, nextAiJobSeq, nextInstallJobSeq, patchHealAt, patchHealReport, setFwCheckCache, setMarketIndexCache, setPatchHealAt, setPatchHealReport } from './server/state.js'
47
- import { routeStateGet, routeDetails } from './server/routes/state.js'
48
- import {routeFrameworkUpgradeStatusGet, routeFrameworkRelaunch, routeFrameworkCheck, routeCompatGate, routeCheckUpdate, routeFrameworkRollback, routeRestart } from './server/routes/framework.js'
49
- import { routeFrameworkUpgrade } from './server/routes/framework-upgrade.js'
50
- import { routeToggle, routeUninstall, routeAdaptUnlock, routeAdaptUnlockAll, routeCleanResiduals, routeSelfUpdate } from './server/routes/plugins.js'
51
- import { routeSearch, routeEnrich, routeRepo, routeSubpackages, routeMarketIndex } from './server/routes/market.js'
52
- import { routeAiConsent, routeAiEmpowerPlan, routeAiEmpowerStatus, routeAiEmpowerList, routeAiEmpowerRun, routeAiEmpowerCancel } from './server/routes/ai.js'
53
- import { routeComponents, routeRepoClone, routeRepoList, routeRepoLandConfig, routeRepoRemove, routeRepoOpen, routeComponentAutostart, routeComponentStart, routeComponentStop, routeComponentStatus } from './server/routes/components.js'
54
- import { routeSourcesGet, routeGiteeOauthUrlGet, routeGiteeOauthCallbackGet, routeRegistryScan, routeSources } from './server/routes/sources.js'
55
- import { routeSkillsInstalledGet, routeSkillRemove, routeSkillToggle } from './server/routes/skills.js'
56
- import { routeInstall, routeInstallStatus } from './server/routes/install.js'
57
- import { detectAdoptablePending, detectCompat, frameworkCompatReportFor, maybeAutoAdaptCompat, rowIdModuleMap } from './server/domain/compat.js'
58
- import { backupProfileSnapshot, currentFrameworkVersion, detectFrameworkUpgrade, preflightDisableIncompatible } from './server/domain/framework.js'
59
- import { listEntries, webPort } from './server/domain/runtime.js'
60
- import { isProtectedModule } from './server/domain/runtime.js'
61
- import { aiEmpowerExecute, aiEmpowerPlan } from './server/domain/ai-run.js'
62
- import { runInstallJob } from './server/domain/install-job.js'
63
- import { runSuiteInstallJob } from './server/domain/suite.js'
64
- import { pnpmRemove, readExtraBundleRows } from './server/domain/install-job.js'
65
- import { DEFAULT_SEARCH } from './server/domain/sources.js'
66
- import { ROUTE_PREFIX, handle } from './server/routes/index.js'
67
-
68
- /** Cordis 插件元信息。 */
69
- export const name = '@noob-stupid/dsh-plugin-console'
70
- export const inject = ['webServer', 'loader']
71
-
72
- /**
73
- * 后台安装任务注册表:请求立即返回,安装继续在服务端执行;
74
- * 面板通过 /install-status 轮询进度(stage + status),离开面板不中断。
75
- */
76
- /** 静态插件索引内存缓存(/market-index 用)。 */
77
-
78
- // ============================================================
79
- // AI 赋能:文档驱动的通用组件部署(读文档 → 出计划 → 确认 → 执行 → 组件控制)
80
- // ============================================================
81
-
82
- /** 应用插件:注册 /plugin-console 路由。 */
83
- export function apply(ctx) {
84
- // 恢复 AI 赋能任务(restart 后 running 置失败,plan-ready/结果保留)
85
- loadAiJobs()
86
- // 升级脚本的「启动失败隔离」记录并入适配门清单(让用户看到谁被自动关了)
87
- // v0.3.44:这里的 catch 不再静默 —— 2026-09-11 那次隔离记录(20 行)就是这样凭空消失的:
88
- // 合并函数在中途抛错、catch 吞掉,界面只剩一个没有解释的【停用】。失败必须留痕。
89
- try { mergeQuarantineRecord(ctx) } catch (error) { logQuarantineMergeError(error) }
90
- // v0.3.45:清单与现实对账 —— 补 moduleName(否则「全家桶一键启用已适配」永远匹配不到),
91
- // 以及把「已经启用、却还挂着 pending」的记录转成已适配(用户实测:启用后重启又变【待适配】)
92
- try {
93
- const fixed = reconcileCompatPending(ctx)
94
- if (fixed.backfilled > 0 || fixed.adopted > 0) {
95
- try { writeFileSync(join(dshHome(), 'plugin-console', 'fw-merge-error.log'), `${new Date().toISOString()} 清单对账:补 moduleName ${fixed.backfilled} 行,已启用转已适配 ${fixed.adopted} 行\n`, { flag: 'a' }) } catch {}
96
- }
97
- } catch (error) { logQuarantineMergeError(error) }
98
- // 启动时清掉上次升级/回滚/重启留下的僵尸计划任务(脚本被强杀时来不及自删)+ 后台清掉 `.trash-*` 降级目录(2026-09-26 加法;覆盖 tmpdir / profile node_modules / skills / repos,即发即忘、绝不阻塞启动)
99
- try { cleanupStaleFwTasks(); startTrashCleanup({ roots: trashScanRoots({ profileDir: profileDirOf(ctx) ?? '', extra: [join(dshHome(), 'skills'), getReposDir()] }) }) } catch {}
100
- // DSH 启动时自动拉起标记了「自启动」的服务器组件(幂等,不阻塞启动)
101
- autostartComponents().catch(() => {})
102
- ctx.effect(() => {
103
- const route = {
104
- kind: 'prefix',
105
- path: ROUTE_PREFIX,
106
- handler: async (req, res) => {
107
- if (!isLoopback(req.socket?.remoteAddress ?? '')) {
108
- sendError(res, 403, '仅允许本机访问')
109
- return
110
- }
111
- const port = webPort(ctx)
112
- const host = typeof req.headers?.host === 'string' ? req.headers.host : ''
113
- if (![`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`].includes(host)) {
114
- sendError(res, 403, 'Host 校验失败(仅允许本机访问)')
115
- return
116
- }
117
- // 安全护栏(issue #9):写路由必须是本机同源请求,防止恶意网页跨站驱动敏感操作。
118
- const method = req.method ?? 'GET'
119
- if (method !== 'GET' && method !== 'HEAD' && !isAllowedWriteOrigin(req, port)) {
120
- sendError(res, 403, '跨站请求被拒绝(Origin/Sec-Fetch-Site 校验)')
121
- return
122
- }
123
- try {
124
- await handle(ctx, req, res)
125
- } catch (error) {
126
- sendError(res, 500, error instanceof Error ? error.message : String(error))
127
- }
128
- },
129
- }
130
- return ctx.webServer.register(route)
131
- }, 'plugin-console: routes')
132
- }
133
-
134
- // ── 路由表(分层 Step 8b:表驱动分发,替代逐条 if 分支)─────────────────────────
135
- // 方法限定且位于 405 守卫之前的分支(只读接口)
136
-
137
- /** 路由处理器需要的宿主服务(仍住在 index.js 的 ctx 相关函数)—— ports 注入点(方案 §四)。
138
- * 用函数而不是模块级常量:这些是函数声明(会提升),但用惰性函数可以彻底避免初始化顺序问题。 */
139
-
140
- /** 框架版本检查缓存(功能包 → 框架 面板用;5 分钟 TTL,避免每次打开面板都打 registry)。 */
141
-
1
+ /**
2
+ * @deepseek-ai/dsh-plugin-console — 插件控制台宿主端。
3
+ *
4
+ * 提供环回 HTTP 路由(前缀 /plugin-console):
5
+ * GET /plugin-console/state 当前插件清单 + 用户补丁层状态
6
+ * POST /plugin-console/toggle 一键启用/停用插件(写 cordis.patch.yml,HMR 生效)
7
+ * POST /plugin-console/search GitHub 仓库搜索(dsh-plugin 相关)
8
+ * POST /plugin-console/repo 读取仓库的 package.json,判断是否可安装
9
+ * POST /plugin-console/install 安装 npm 包(或 git 仓库)并追加启用条目
10
+ *
11
+ * 插件开关的机制:用户补丁层 cordis.patch.yml 是逐键覆盖(id-targeted patch),
12
+ * 追加 `- id: X` + `disabled: true` 即可停用任意行(含 bundle 行与用户 insert 行),
13
+ * 移除该块即恢复;HMR 监视器会自动重组合,无需重启。
14
+ */
15
+ import { readFile, writeFile } from 'node:fs/promises'
16
+ import { readFileSync, writeFileSync, existsSync, statSync, rmSync, readdirSync, mkdirSync, copyFileSync, realpathSync } from 'node:fs'
17
+ import { execFile, spawn } from 'node:child_process'
18
+ import { promisify } from 'node:util'
19
+ import { dirname, join, basename, sep } from 'node:path'
20
+ import { tmpdir, homedir } from 'node:os'
21
+ import { fileURLToPath, pathToFileURL } from 'node:url'
22
+ import { createRequire } from 'node:module'
23
+ import { request as httpsRequest } from 'node:https'
24
+ import { aiJobsFile, componentsFile, dshHome, entryPkgMeta, findPatchPath, marketIndexCacheFile, packageNameOf, pluginRoot, profileDirOf, repoLandConfFile, resolvePackageJson, rowIdOf } from './server/infra/paths.js'
25
+ import { isFrameworkVersionNewer, parseFrameworkVersion, semverRangeMatch, semverRangeMatchLoose } from './server/infra/semver.js'
26
+ import { GITHUB_API, curlJson, fetchJsonUrl, githubJson, postJsonUrl, rawTextWithFallback } from './server/infra/http.js'
27
+ import { cleanupStalePackageDir, copyTree, queuedWrite, startTrashCleanup, trashScanRoots } from './server/infra/fsx.js'
28
+ import { execFileAsync, gitEnv, processAlive } from './server/infra/exec.js'
29
+ import { escapeRegExp, maskUrl } from './server/infra/mask.js'
30
+ import { isAllowedWriteOrigin, isLoopback, readBody, sendError, sendJson } from './server/infra/httpd.js'
31
+ import { DEFAULT_SOURCES, consumeGiteeOAuthState, createGiteeOAuthState, gitCloneUrls, giteeStatusView, isAllowedGitSourceUrl, isAllowedSourceUrl, maskSources, orderedRegistries, readGiteeConfig, readSources, writeSources } from './server/domain/sources.js'
32
+ import { CORE_PATCH_ROW_IDS, appendInsert, disableEntry, enableEntry, healPatchSafety, readPatchState, removeDisableBlock, removeInsertRow, sanitizePatchText, syncNameFromNote } from './server/domain/patch.js'
33
+ import { enrichItems, fetchRepoPackage, fetchSubpackageNames, githubRepoInfo, normalizePlatformItems, readPluginDetails, subpackageCandidates } from './server/domain/market.js'
34
+ import { SKILL_TOPICS, detectSkillRepo, fetchSkillMeta, listInstalledSkills, runSkillInstallJob, setSkillEnabled } from './server/domain/skills.js'
35
+ import { autostartComponents, compFind, compStart, compStatus, compStop, compUiUrl, compUpsert, findComponents } from './server/domain/components.js'
36
+ import { getReposDir, gitCloneRepo, listLandedRepos, setReposDir } from './server/domain/repoland.js'
37
+ import { DEFAULT_BUNDLES, addBundleToManifest, backfillMissingDeps, curlManualInstall, detectBundleOnly, ensureBundlePatchIntegrity, githubReleaseInstall, installJobView, pnpmInstall, raceInstallChannels, readBundlePatchRefNames, readExtraBundleOwners, readGithubAuth, syncAggregateSubpackageVersions } from './server/domain/install.js'
38
+ import { findPresetDirs, readGitmodules } from './server/domain/suite.js'
39
+ import { FIBER_PHASE, SUPPORTED_WEB_APP_PATTERN, checkPluginFrameworkCompat, frameworkCheckPromptText, isFrameworkOwnedPackage, probePluginImport, readCompatGate, readCompatPending, writeCompatGate, writeCompatPending } from './server/domain/compat.js'
40
+ import { logQuarantineMergeError, markPendingAdopted, mergeQuarantineRecord, planQuarantine, readQuarantineRecord, reconcileCompatPending } from './server/domain/quarantine.js'
41
+ import { FRAMEWORK_BACKUP_ROOT, FRAMEWORK_STATE_FILE, applyFrameworkTolerancePatchOnce, checkpointFrameworkTree, cleanupStaleFwTasks, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules } from './server/domain/framework.js'
42
+ import { migrateAgentConfigsForUpgrade } from './server/domain/presets.js'
43
+ import { GITEE_AUTH_URL, GITEE_TOKEN_URL } from './server/domain/sources.js'
44
+ import { aiEmpowerPresetFor, aiJobs, builtinPlanFor, isAllowedWritePath, isSafeRunCmd, loadAiJobs, parsePlanJson, resolvePlaceholders, resolvePythonPath, saveAiJobs } from './server/domain/ai.js'
45
+ import { jobLog, waitHealth } from './server/domain/jobs.js'
46
+ import { fwCheckCache, installJobs, marketIndexCache, nextAiJobSeq, nextInstallJobSeq, patchHealAt, patchHealReport, setFwCheckCache, setMarketIndexCache, setPatchHealAt, setPatchHealReport } from './server/state.js'
47
+ import { routeStateGet, routeDetails } from './server/routes/state.js'
48
+ import {routeFrameworkUpgradeStatusGet, routeFrameworkRelaunch, routeFrameworkCheck, routeCompatGate, routeCheckUpdate, routeFrameworkRollback, routeRestart } from './server/routes/framework.js'
49
+ import { routeFrameworkUpgrade } from './server/routes/framework-upgrade.js'
50
+ import { routeToggle, routeUninstall, routeAdaptUnlock, routeAdaptUnlockAll, routeCleanResiduals, routeSelfUpdate } from './server/routes/plugins.js'
51
+ import { routeSearch, routeEnrich, routeRepo, routeSubpackages, routeMarketIndex } from './server/routes/market.js'
52
+ import { routeAiConsent, routeAiEmpowerPlan, routeAiEmpowerStatus, routeAiEmpowerList, routeAiEmpowerRun, routeAiEmpowerCancel } from './server/routes/ai.js'
53
+ import { routeComponents, routeRepoClone, routeRepoList, routeRepoLandConfig, routeRepoRemove, routeRepoOpen, routeComponentAutostart, routeComponentStart, routeComponentStop, routeComponentStatus } from './server/routes/components.js'
54
+ import { routeSourcesGet, routeGiteeOauthUrlGet, routeGiteeOauthCallbackGet, routeRegistryScan, routeSources } from './server/routes/sources.js'
55
+ import { routeSkillsInstalledGet, routeSkillRemove, routeSkillToggle } from './server/routes/skills.js'
56
+ import { routeInstall, routeInstallStatus } from './server/routes/install.js'
57
+ import { detectAdoptablePending, detectCompat, frameworkCompatReportFor, maybeAutoAdaptCompat, rowIdModuleMap } from './server/domain/compat.js'
58
+ import { backupProfileSnapshot, currentFrameworkVersion, detectFrameworkUpgrade, preflightDisableIncompatible } from './server/domain/framework.js'
59
+ import { listEntries, webPort } from './server/domain/runtime.js'
60
+ import { isProtectedModule } from './server/domain/runtime.js'
61
+ import { aiEmpowerExecute, aiEmpowerPlan } from './server/domain/ai-run.js'
62
+ import { runInstallJob } from './server/domain/install-job.js'
63
+ import { runSuiteInstallJob } from './server/domain/suite.js'
64
+ import { pnpmRemove, readExtraBundleRows } from './server/domain/install-job.js'
65
+ import { DEFAULT_SEARCH } from './server/domain/sources.js'
66
+ import { ROUTE_PREFIX, handle } from './server/routes/index.js'
67
+
68
+ /** Cordis 插件元信息。 */
69
+ export const name = '@noob-stupid/dsh-plugin-console'
70
+ export const inject = ['webServer', 'loader']
71
+
72
+ /**
73
+ * 后台安装任务注册表:请求立即返回,安装继续在服务端执行;
74
+ * 面板通过 /install-status 轮询进度(stage + status),离开面板不中断。
75
+ */
76
+ /** 静态插件索引内存缓存(/market-index 用)。 */
77
+
78
+ // ============================================================
79
+ // AI 赋能:文档驱动的通用组件部署(读文档 → 出计划 → 确认 → 执行 → 组件控制)
80
+ // ============================================================
81
+
82
+ /** 应用插件:注册 /plugin-console 路由。 */
83
+ export function apply(ctx) {
84
+ // 恢复 AI 赋能任务(restart 后 running 置失败,plan-ready/结果保留)
85
+ loadAiJobs()
86
+ // 升级脚本的「启动失败隔离」记录并入适配门清单(让用户看到谁被自动关了)
87
+ // v0.3.44:这里的 catch 不再静默 —— 2026-09-11 那次隔离记录(20 行)就是这样凭空消失的:
88
+ // 合并函数在中途抛错、catch 吞掉,界面只剩一个没有解释的【停用】。失败必须留痕。
89
+ try { mergeQuarantineRecord(ctx) } catch (error) { logQuarantineMergeError(error) }
90
+ // v0.3.45:清单与现实对账 —— 补 moduleName(否则「全家桶一键启用已适配」永远匹配不到),
91
+ // 以及把「已经启用、却还挂着 pending」的记录转成已适配(用户实测:启用后重启又变【待适配】)
92
+ try {
93
+ const fixed = reconcileCompatPending(ctx)
94
+ if (fixed.backfilled > 0 || fixed.adopted > 0) {
95
+ try { writeFileSync(join(dshHome(), 'plugin-console', 'fw-merge-error.log'), `${new Date().toISOString()} 清单对账:补 moduleName ${fixed.backfilled} 行,已启用转已适配 ${fixed.adopted} 行\n`, { flag: 'a' }) } catch {}
96
+ }
97
+ } catch (error) { logQuarantineMergeError(error) }
98
+ // 启动时清掉上次升级/回滚/重启留下的僵尸计划任务(脚本被强杀时来不及自删)+ 后台清掉 `.trash-*` 降级目录(2026-09-26 加法;覆盖 tmpdir / profile node_modules / skills / repos,即发即忘、绝不阻塞启动)
99
+ try { cleanupStaleFwTasks(); startTrashCleanup({ roots: trashScanRoots({ profileDir: profileDirOf(ctx) ?? '', extra: [join(dshHome(), 'skills'), getReposDir()] }) }) } catch {}
100
+ // DSH 启动时自动拉起标记了「自启动」的服务器组件(幂等,不阻塞启动)
101
+ autostartComponents().catch(() => {})
102
+ ctx.effect(() => {
103
+ const route = {
104
+ kind: 'prefix',
105
+ path: ROUTE_PREFIX,
106
+ handler: async (req, res) => {
107
+ if (!isLoopback(req.socket?.remoteAddress ?? '')) {
108
+ sendError(res, 403, '仅允许本机访问')
109
+ return
110
+ }
111
+ const port = webPort(ctx)
112
+ const host = typeof req.headers?.host === 'string' ? req.headers.host : ''
113
+ if (![`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`].includes(host)) {
114
+ sendError(res, 403, 'Host 校验失败(仅允许本机访问)')
115
+ return
116
+ }
117
+ // 安全护栏(issue #9):写路由必须是本机同源请求,防止恶意网页跨站驱动敏感操作。
118
+ const method = req.method ?? 'GET'
119
+ if (method !== 'GET' && method !== 'HEAD' && !isAllowedWriteOrigin(req, port)) {
120
+ sendError(res, 403, '跨站请求被拒绝(Origin/Sec-Fetch-Site 校验)')
121
+ return
122
+ }
123
+ try {
124
+ await handle(ctx, req, res)
125
+ } catch (error) {
126
+ sendError(res, 500, error instanceof Error ? error.message : String(error))
127
+ }
128
+ },
129
+ }
130
+ return ctx.webServer.register(route)
131
+ }, 'plugin-console: routes')
132
+ }
133
+
134
+ // ── 路由表(分层 Step 8b:表驱动分发,替代逐条 if 分支)─────────────────────────
135
+ // 方法限定且位于 405 守卫之前的分支(只读接口)
136
+
137
+ /** 路由处理器需要的宿主服务(仍住在 index.js 的 ctx 相关函数)—— ports 注入点(方案 §四)。
138
+ * 用函数而不是模块级常量:这些是函数声明(会提升),但用惰性函数可以彻底避免初始化顺序问题。 */
139
+
140
+ /** 框架版本检查缓存(功能包 → 框架 面板用;5 分钟 TTL,避免每次打开面板都打 registry)。 */
141
+
@@ -1,6 +1,6 @@
1
1
  // L1 · domain —— ai-run.js(AI 赋能的执行侧:规划 / 执行 / 修复 / 单步运行)。
2
2
  // 分层 Step 8c-2 从 lib/index.js 搬出;形参由 ctx 收窄为 ports,只搬移未改逻辑。
3
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
3
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
4
4
 
5
5
  import { writeFileSync, existsSync, statSync, rmSync, mkdirSync } from 'node:fs'
6
6
  import { readFile } from 'node:fs/promises'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— ai.js(AI 赋能的纯逻辑部分:任务表与视图、DeepSeek 密钥与配置、占位符/Python/VLM 解析、写入白名单与运行白名单、内置计划模板、计划 JSON 解析;分层 Step 7 从 lib/index.js 搬出,只搬移未改逻辑。注:aiEmpowerPlan/Execute、runAiStep、aiRepair 吃运行上下文,留到 Step 8)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
5
5
  import { dirname, join, basename, resolve } from 'node:path'
@@ -126,15 +126,41 @@ function resolvePlaceholders(value, jobOrEnv) {
126
126
  .replaceAll('${python}', python)
127
127
  .replaceAll('${scripts}', join(dirname(python), 'Scripts'))
128
128
  .replaceAll('${node}', process.execPath)
129
+ .replaceAll('${asciiData}', openVikingDataRoot())
129
130
  .replaceAll('${deepseekKey}', readDeepSeekKey())
130
131
  }
131
132
 
133
+ /** 解析 Python 解释器:候选按平台分开,**不含任何开发机专有路径**。
134
+ * win32:用户级安装目录(AppData\Local\Programs\Python)+ PATH 上的 python.exe;
135
+ * POSIX:/usr/bin/python3、/usr/local/bin/python3、PATH 上的 python3/python。
136
+ * 原实现第一条写死开发机的绝对路径(`<盘符>:\python314\python.exe`),且三个候选全是 Windows 形态 ——
137
+ * 非 Windows 上永远解析不到,`${python}` 会退化成 Windows 专有名字。
138
+ * 解析不到时返回**本平台的命令名**(由执行阶段如实报错),不在这里抛。 */
132
139
  function resolvePythonPath() {
133
- const candidates = [join('E:', 'python314', 'python.exe'), join(homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python314', 'python.exe'), 'python.exe']
140
+ const win = process.platform === 'win32'
141
+ const candidates = win
142
+ ? [
143
+ join(homedir(), 'AppData', 'Local', 'Programs', 'Python', 'Python314', 'python.exe'),
144
+ 'python.exe',
145
+ ]
146
+ : ['/usr/bin/python3', '/usr/local/bin/python3', 'python3', 'python']
134
147
  for (const c of candidates) {
135
148
  if (existsSync(c)) return c
136
149
  }
137
- return 'python.exe'
150
+ return win ? 'python.exe' : 'python3'
151
+ }
152
+
153
+ /** OpenViking 数据根:**必须纯 ASCII**(home 含非 ASCII 字符时,各语言的 Rust/原生向量库
154
+ * 写盘会报 UnicodeDecodeError)。三种来源,**不含任何写死的盘符**:
155
+ * ① 环境变量 DSH_OPENVIKING_DATA_ROOT(用户显式指定,最高优先)
156
+ * ② win32 历史默认:D:/OpenVikingData(保持既有实例的数据位置不变)
157
+ * ③ 其他平台:${home}/.openviking/ascii-data
158
+ * 原实现无条件返回 'D:/OpenVikingData':非 Windows 上它是个**相对路径**,会被写进 CWD
159
+ * (服务进程的工作目录),在别人的机器上根本不是"ASCII 数据根"。 */
160
+ function openVikingDataRoot() {
161
+ const override = process.env.DSH_OPENVIKING_DATA_ROOT
162
+ if (typeof override === 'string' && override.trim() !== '') return override.trim()
163
+ return process.platform === 'win32' ? 'D:/OpenVikingData' : join(homedir(), '.openviking', 'ascii-data')
138
164
  }
139
165
 
140
166
  /** 写入路径白名单:只允许 DSH profile、~/.dsh、~/.openviking、~/.cache/openviking、ASCII 数据根。 */
@@ -148,13 +174,13 @@ function isAllowedWritePath(p, profileDir) {
148
174
  norm(homedir()) + '/.local/share/openviking',
149
175
  norm(homedir()) + '/.dsh/repos',
150
176
  norm(profileDir),
151
- 'D:/OpenVikingData',
177
+ norm(openVikingDataRoot()),
152
178
  ]
153
179
  return roots.some((root) => target === root || target.startsWith(root + '/'))
154
180
  }
155
181
 
156
- /** run-cmd 可执行文件白名单(按 basename)。 */
157
- const ALLOWED_RUN_FILES = new Set(['curl.exe', 'curl', 'git.exe', 'git', 'node.exe', 'node', 'python.exe', 'python', 'py.exe', 'py', 'gh.exe', 'gh', 'npm.cmd', 'npm', 'ov.cmd', 'ov'])
182
+ /** run-cmd 可执行文件白名单(按 basename;含 POSIX 名 python3,否则 ${python}=python3 会被自己拒掉)。 */
183
+ const ALLOWED_RUN_FILES = new Set(['curl.exe', 'curl', 'git.exe', 'git', 'node.exe', 'node', 'python.exe', 'python3', 'python', 'py.exe', 'py', 'gh.exe', 'gh', 'npm.cmd', 'npm', 'ov.cmd', 'ov'])
158
184
 
159
185
  const DESTRUCTIVE_RE = /(^|\s)(rm\s+(-[a-zA-Z]*r)|rmdir\s+\/s|del\s+\/s|format\s+[a-zA-Z]:|Remove-Item\b|taskkill\s+\/im|reg\s+delete|schtasks\s+\/delete|shutdown|cipher\s+\/w)/iu
160
186
 
@@ -178,7 +204,7 @@ function builtinPlanFor(source) {
178
204
  .replace(/\/$/u, '')
179
205
  if (norm !== 'openviking' && norm !== 'volcengine/openviking') return null
180
206
  const home = homedir()
181
- const storage = /^[\x00-\x7F]+$/u.test(home) ? `${home}/.openviking/data` : 'D:/OpenVikingData'
207
+ const storage = /^[\x00-\x7F]+$/u.test(home) ? `${home}/.openviking/data` : openVikingDataRoot()
182
208
  const vlm = resolveVlmForOpenViking()
183
209
  // 注意:路径必须在此处(JSON.stringify 之前)使用真实值,由 stringify 统一转义;
184
210
  // 不能在 content 中保留 ${home} 占位符——写文件时替换会把反斜杠路径变成非法 JSON 转义。
@@ -209,10 +235,12 @@ function aiEmpowerPresetFor(source) {
209
235
  const common = [
210
236
  '## 本机环境事实(务必遵守)',
211
237
  '- 网络:pypi.org 不通、清华镜像 403;pip 必须用 `--index-url https://mirrors.aliyun.com/pypi/simple/`。npm registry.npmjs.org 可能黑洞,pnpm 用 `--registry https://registry.npmmirror.com`。huggingface.co 被墙,模型文件一律用 `https://hf-mirror.com/<同一路径>`(执行器会自动把 huggingface.co 换成 hf-mirror.com,计划里也可直接写 hf-mirror)。',
212
- // 0.5.28 改错:这条原本把它写成"本机用户名含中文(花火)"的**个案**,读起来像只在
238
+ // 0.5.28 改错:这条原本把它写成"本机用户名含非 ASCII 字符"的**个案**,读起来像只在
213
239
  // 那台机器上成立。真正的规则是通用的:只要**写盘路径**里出现非 ASCII 字符,各语言的
214
240
  // Rust/原生向量库就可能报 UnicodeDecodeError —— 与用户名具体是不是中文无关。
215
- '- 用户名/路径含非 ASCII 字符时:任何写盘路径若含非 ASCII 字符,各语言的 Rust/原生向量库会报 UnicodeDecodeError——服务数据目录必须纯 ASCII(如 `D:/OpenVikingData`)。',
241
+ // 2026-09-29 改错:ASCII 数据根不再写死某个盘符,改用 `${asciiData}` 占位符
242
+ // (由 resolvePlaceholders 解析成"本平台/本用户"的 ASCII 数据根,见 openVikingDataRoot)。
243
+ '- 用户名/路径含非 ASCII 字符时:任何写盘路径若含非 ASCII 字符,各语言的 Rust/原生向量库会报 UnicodeDecodeError——服务数据目录必须纯 ASCII,用 `${asciiData}` 引用控制台给出的 ASCII 数据根(可用环境变量 DSH_OPENVIKING_DATA_ROOT 覆盖)。',
216
244
  '- 凭据复用:DSH 的 DeepSeek 密钥在 `${home}/.dsh/.credentials.yaml`(YAML 行 `DEEPSEEK_API_KEY: sk-...`);模型选择在 `${home}/.dsh/settings.yaml` 的 `agent-default-model`(provider deepseek-official)。对外 OpenAI 兼容接口:api_base `https://api.deepseek.com`,模型名 `deepseek-v4-flash-vision-exp`。',
217
245
  '- 步骤解释用中文;每个动作一个步骤;步骤数尽量少;不得包含任何破坏性命令;服务类组件必须配 start-service + wait-health。',
218
246
  ]
@@ -222,8 +250,8 @@ function aiEmpowerPresetFor(source) {
222
250
  '## OpenViking 已验证的部署事实(v0.4.17,Python 3.14)',
223
251
  '- 安装:`pip install openviking[local-embed]`(含 llama-cpp-python 0.3.35,cp310-abi3 轮子可用;无需加 --force-reinstall)。',
224
252
  '- 嵌入模型:`bge-small-zh-v1.5-f16.gguf`(47MB,512 维中文)下载到 `${home}/.cache/openviking/models/`(写 download 步骤;URL 写 huggingface.co 会被执行器自动换 hf-mirror.com)。',
225
- '- ov.conf 路径 `${home}/.openviking/ov.conf`,内容:vlm = { provider: openai, api_base: https://api.deepseek.com, model: deepseek-v4-flash-vision-exp, api_key: 从 `${home}/.dsh/.credentials.yaml` 读取并内联 };embedding.dense = { provider: local, model: bge-small-zh-v1.5-f16, model_path: <上面的 gguf 路径> };server = { host: 127.0.0.1, port: 1933 }(不要写 root_api_key,dev 免鉴权模式自动启用);storage.workspace = `${home}` 含非 ASCII 时写 `D:/OpenVikingData`,否则 `${home}/.openviking/data`。',
226
- '- 启动:start-service,file=`${python}`,args=`["-m","openviking_cli.server_bootstrap"]`,cwd=`${home}/.openviking`,healthUrl=`http://127.0.0.1:1933/health`,port=1933。服务器启动首次会建 `D:/OpenVikingData` 数据目录。',
253
+ '- ov.conf 路径 `${home}/.openviking/ov.conf`,内容:vlm = { provider: openai, api_base: https://api.deepseek.com, model: deepseek-v4-flash-vision-exp, api_key: 从 `${home}/.dsh/.credentials.yaml` 读取并内联 };embedding.dense = { provider: local, model: bge-small-zh-v1.5-f16, model_path: <上面的 gguf 路径> };server = { host: 127.0.0.1, port: 1933 }(不要写 root_api_key,dev 免鉴权模式自动启用);storage.workspace = `${home}` 含非 ASCII 时写 `${asciiData}`,否则 `${home}/.openviking/data`。',
254
+ '- 启动:start-service,file=`${python}`,args=`["-m","openviking_cli.server_bootstrap"]`,cwd=`${home}/.openviking`,healthUrl=`http://127.0.0.1:1933/health`,port=1933。服务器启动首次会建 `${asciiData}` 数据目录。',
227
255
  '- 部署完成后惯例:重启 DSH 会话后 mcp__openviking__* 工具出现,`${home}/.openviking/pending` 的积压消息会在新会话启动时自动回放——把这写进总结。',
228
256
  ].join('\n')
229
257
  }
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— compat.js(框架兼容判定与适配门:设置 API 扫描 / 兼容门读写 / 待适配记录 / 隔离记录合并的前置部分 / 启动失败分析;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑。注:detectCompat 等吃运行上下文的留到 Step 8)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync } from 'node:fs'
5
5
  import { readFile } from 'node:fs/promises'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— components.js(服务器组件:注册表 / 增删改 / 进程启停 / 状态 / 自启动;分层 Step 4 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
5
5
  import { spawn } from 'node:child_process'
@@ -11,7 +11,7 @@
11
11
  // lock(删 lock / 清 node_modules / 换机 / CI)→ `ERR_PNPM_FETCH_404`,而报错指向 npm registry,
12
12
  // 用户根本联想不到是几周前面板安装改写造成的。
13
13
  //
14
- // 两条硬约束(本机真 pnpm 10.34.5 实测矩阵见 D:\dsh\dsh-plugin-hub-plan\refactor-bugs.zh.md 第 23 节):
14
+ // 两条硬约束(真 pnpm 10.34.5 实测矩阵见仓库外私有方案稿第 23 节):
15
15
  // ① 写回前必须确认「这个包的**这个版本**」在 registry 可解析,否则**绝不**写裸版本号;
16
16
  // ② 不可解析时**也不能**写 tarball URL:pnpm 10 对 direct-URL 依赖只在冷缓存真下载时记 integrity,
17
17
  // 命中缓存重写 lock 时 resolution 里没有 integrity → `ERR_PNPM_MISSING_TARBALL_INTEGRITY`,
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— framework.js(升级三件套的可搬部分:状态/备份路径、dsh 可执行文件定位、app boot 定位、容忍补丁、版本比较、重启前奏;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑。注:detectFrameworkUpgrade / backupProfileSnapshot / currentFrameworkVersion 吃运行上下文,留到 Step 8)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, copyFileSync } from 'node:fs'
5
5
  import { execFile } from 'node:child_process'
@@ -27,6 +27,27 @@ const FRAMEWORK_STATE_FILE = () => join(dshHome(), 'plugin-console', 'framework-
27
27
 
28
28
  const FRAMEWORK_BACKUP_ROOT = () => join(dshHome(), 'plugin-console', 'framework-backups')
29
29
 
30
+ /** npm 缓存下的 npx 缓存目录候选(**不写死盘符/用户名**)。
31
+ * npm 的 cache 位置本来就是用户配置(把 cache 指到别的盘是常见做法),所以按 npm 自己的
32
+ * 配置来源读取:环境变量 npm_config_cache / NPM_CONFIG_CACHE,以及用户级 ~/.npmrc 的 `cache=`。
33
+ * (原实现把开发机上某个盘符下的 `node_cache\_npx` 直接写进源码,随 npm 发给所有用户。) */
34
+ function npmNpxCacheRoots() {
35
+ const roots = []
36
+ const push = (value) => {
37
+ if (typeof value !== 'string') return
38
+ const dir = value.trim().replace(/^["']|["']$/gu, '').replace(/^~(?=[\\/]|$)/u, homedir())
39
+ if (dir !== '') roots.push(join(dir, '_npx'))
40
+ }
41
+ for (const key of ['npm_config_cache', 'NPM_CONFIG_CACHE']) push(process.env[key])
42
+ try {
43
+ for (const line of readFileSync(join(homedir(), '.npmrc'), 'utf8').split(/\r?\n/u)) {
44
+ const hit = /^\s*cache\s*=\s*(.*?)\s*$/iu.exec(line)
45
+ if (hit) push(hit[1])
46
+ }
47
+ } catch {}
48
+ return roots
49
+ }
50
+
30
51
  /** 定位 @deepseek-ai/dsh-app-boot(与 @deepseek-ai/dsh 同级)。 */
31
52
  function locateAppBootFile(baseUrl) {
32
53
  try {
@@ -35,15 +56,26 @@ function locateAppBootFile(baseUrl) {
35
56
  const candidate = join(dirname(dshPkg), 'dsh-app-boot', 'lib', 'index.js')
36
57
  if (existsSync(candidate)) return candidate
37
58
  } catch {}
59
+ // 缓存根一律由「环境变量 / 用户配置 / 用户目录」派生,**不含任何写死的盘符或开发机路径**:
60
+ // · NODE_CACHE:npx 缓存根的可选覆盖(直接就是 _npx 那一层)
61
+ // · npm_config_cache / ~/.npmrc 的 cache=:npm 自己配置的缓存根(下面挂 _npx)
62
+ // · ~/.npm/_npx:npm/npx 的默认用户级缓存(POSIX 与 Windows 同一处)
63
+ // · %LOCALAPPDATA%\node_cache\_npx:缓存被重定向到 LOCALAPPDATA 时的位置(仅 Windows 有意义)
64
+ // 原实现第一条写死开发机上的缓存绝对路径(某个盘符下的 node_cache\_npx),且 LOCALAPPDATA
65
+ // 缺失时会拼出**相对路径** 'node_cache/_npx'(相对 CWD)——两处都已去掉。
38
66
  const cacheRoots = [
39
67
  process.env.NODE_CACHE || '',
40
- 'D:\\node_cache\\_npx',
68
+ ...npmNpxCacheRoots(),
41
69
  join(homedir(), '.npm', '_npx'),
42
- join(process.env.LOCALAPPDATA || '', 'node_cache', '_npx'),
70
+ process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'node_cache', '_npx') : '',
43
71
  ].filter(Boolean)
44
72
  for (const root of cacheRoots) {
45
73
  if (!existsSync(root)) continue
46
- for (const entry of readdirSync(root, { withFileTypes: true })) {
74
+ // 读目录失败(权限/竞态)只跳过这一条候选,绝不抛给调用方——本函数的契约是
75
+ // 「定位不到就返回 null」(调用方据此如实报「dsh-app-boot 未找到」)。
76
+ let entries = []
77
+ try { entries = readdirSync(root, { withFileTypes: true }) } catch { continue }
78
+ for (const entry of entries) {
47
79
  if (!entry.isDirectory()) continue
48
80
  const candidate = join(root, entry.name, 'node_modules', '@deepseek-ai', 'dsh-app-boot', 'lib', 'index.js')
49
81
  if (existsSync(candidate)) return candidate
@@ -141,7 +173,7 @@ function checkpointFrameworkTree(fwRoot, destRoot) {
141
173
  *
142
174
  * 事故现场(官方桌面端实例,127.0.0.1:19387,20:28):
143
175
  * 桌面端里 DSH host 是 **Electron 二进制**跑的 asar 内入口
144
- * (`D:\dsh-desktop\DeepSeek Harness.exe --expose-internals …\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\index.js …`),
176
+ * (`<桌面端安装目录>\DeepSeek Harness.exe --expose-internals …\app.asar\dsh\node_modules\@deepseek-ai\dsh-desktop-host\lib\index.js …`),
145
177
  * 不是 `node bin.js web`。我们的「重启服务 / 回滚 / 框架升级重启」那一套会
146
178
  * `Stop-Process` 掉 host,然后 `Resolve-DshBin` 三种方式全找不到 bin.js
147
179
  * (桌面端 profile 的 node_modules 里根本没有 `@deepseek-ai` 作用域)→ 拉不起来 →
@@ -212,6 +244,33 @@ function shellHostedRefusal(deps) {
212
244
  }
213
245
  }
214
246
 
247
+ /**
248
+ * 非 Windows 平台的标准拒绝载荷(**纯数据,不碰 HTTP**):路由层负责翻成响应。
249
+ * 返回 null = win32(放行,行为与旧实现一字不改)。
250
+ *
251
+ * 为什么必须拒绝(2026-09-29 改错):一键升级/回滚通道整体建立在 Windows 机制上 ——
252
+ * `schtasks.exe` 计划任务(脱离服务进程树)+ PowerShell 脚本 + `Get-NetTCPConnection` 探端口。
253
+ * 非 Windows 上 execFile('schtasks.exe') 必然 ENOENT,而旧实现先 writeFileSync 一个
254
+ * 「脚本已通过 detached 启动」的状态文件、再去 exec 不存在的 powershell.exe,
255
+ * 最后**照样** `upgraded = true` 并回 `ok: true, steps: ['框架升级脚本已启动…']` ——
256
+ * 用户看到"已启动",实际一个字节都没动(静默失败 + 假成功)。宁可明确说"不支持"。
257
+ */
258
+ function platformUpgradeRefusal() {
259
+ if (process.platform === 'win32') return null
260
+ return {
261
+ error: '当前平台暂不支持「一键框架升级/回滚」:该通道依赖 Windows 计划任务(schtasks.exe)与 PowerShell',
262
+ details: {
263
+ code: 'unsupported-platform',
264
+ platform: process.platform,
265
+ manual: [
266
+ '手动升级框架:npm i -g @deepseek-ai/dsh@latest(用 npx 的实例:清掉 npx 缓存后重跑 npx @deepseek-ai/dsh web)',
267
+ '升级前请自行备份 profile 目录(~/.dsh/profiles/<profile>)与 cordis.patch.yml',
268
+ '升级完成后本控制台仍会自动:备份新版本配置快照、重打框架补丁(issue #5 容错)、给出适配提示',
269
+ ],
270
+ },
271
+ }
272
+ }
273
+
215
274
  /**
216
275
  * 生成「拉起 DSH 服务」的 PowerShell 前导块(升级脚本 / 回滚脚本共用,v0.3.37 事故修复)。
217
276
  *
@@ -477,4 +536,4 @@ async function preflightDisableIncompatible({ ports, profileDir, patchPath, targ
477
536
  }
478
537
  return { disabled, skipped }
479
538
  }
480
- export { FRAMEWORK_STATE_FILE, FRAMEWORK_BACKUP_ROOT, SHELL_HOSTED_NOTICE, detectHostShape, shellHostedRefusal, cleanupStaleFwTasks, resolveDshBin, locateAppBootFile, applyFrameworkTolerancePatchOnce, resolveFrameworkRootNodeModules, checkpointFrameworkTree, relaunchPrelude, isVersionAtLeast, pickFrameworkTarget, resolveFrameworkUpgradePlan, currentFrameworkVersion, backupProfileSnapshot, detectFrameworkUpgrade, preflightDisableIncompatible }
539
+ export { FRAMEWORK_STATE_FILE, FRAMEWORK_BACKUP_ROOT, SHELL_HOSTED_NOTICE, detectHostShape, shellHostedRefusal, platformUpgradeRefusal, cleanupStaleFwTasks, resolveDshBin, locateAppBootFile, applyFrameworkTolerancePatchOnce, resolveFrameworkRootNodeModules, checkpointFrameworkTree, relaunchPrelude, isVersionAtLeast, pickFrameworkTarget, resolveFrameworkUpgradePlan, currentFrameworkVersion, backupProfileSnapshot, detectFrameworkUpgrade, preflightDisableIncompatible }
@@ -1,6 +1,6 @@
1
1
  // L1 · domain —— install-job.js(安装任务编排:多通道安装 + 套装补丁 + 适配门 + 收尾)。
2
2
  // 分层 Step 8c-2 从 lib/index.js 搬出;形参由 ctx 收窄为 ports(调用方注入的窄接口),只搬移未改逻辑。
3
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
3
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
4
4
 
5
5
  import { readFileSync, existsSync } from 'node:fs'
6
6
  import { readFile } from 'node:fs/promises'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— install.js(插件安装/卸载全链路:pnpm / curl 手动 / GitHub Release 三通道 + 包盒校验 + 套装补丁完整性;分层 Step 5 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync, mkdirSync } from 'node:fs'
5
5
  import { readFile, writeFile } from 'node:fs/promises'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— jobs.js(任务辅助:日志追加 + 健康等待轮询)。方案原稿把 jobLog 归 ai.js、waitHealth 归 components.js,但这俩是 install / suite / ai 三个域共用的,埋在任何一个域里都会造成跨域反向依赖 —— 故单独成模块(偏离已记录)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
 
5
5
  function jobLog(job, line) {
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— market.js(分层 Step 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
5
5
  import { readFile } from 'node:fs/promises'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— patch.js(由 Step 2 从 lib/index.js 原样切出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L1 · domain
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFile, writeFile } from 'node:fs/promises'
5
5
  import { dirname } from 'node:path'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— presets.js(预设/agent 配置迁移:config 文件收集、persona 迁移、升级期迁移;分层 Step 6 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync, copyFileSync } from 'node:fs'
5
5
  import { join } from 'node:path'
@@ -443,9 +443,9 @@ async function downloadReleaseArtifact(url, dest, options = {}) {
443
443
  * node_modules(包根本身由 paths.js 的 pluginRoot() 解析——全仓库只有那一处算包根)。
444
444
  * 返回 `<root>/<packageName>`。
445
445
  * ★ 特判的判据必须包含"自身确实住在某个 node_modules 里"(dirname 的 basename 为 node_modules):
446
- * 旧判据只有 `existsSync(<pkg>/package.json)`,而任何**开发检出**(D:\dsh\dsh-plugin-hub-refactor 这种
447
- * 不在 node_modules 下的目录)都满足它 → 目标会被算成检出的**父目录**,release 通道装一次插件就往
448
- * `D:\dsh\<包名>` 写一份。生产布局不变:宿主根层 `<host>/node_modules/<pkg>` 仍然命中特判。 */
446
+ * 旧判据只有 `existsSync(<pkg>/package.json)`,而任何**开发检出**(不在 node_modules 下的目录,
447
+ * 例如 `~/dev/<包名>`)都满足它 → 目标会被算成检出的**父目录**,release 通道装一次插件就往
448
+ * `~/dev/<包名>` 写一份。生产布局不变:宿主根层 `<host>/node_modules/<pkg>` 仍然命中特判。 */
449
449
  function releaseInstallTarget(profileDir, packageName) {
450
450
  let targetRoot = join(profileDir, 'node_modules')
451
451
  try {
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— repoland.js(仓库落地:落地目录配置 / 已落地列表 / 克隆;分层 Step 4 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, renameSync, statSync } from 'node:fs'
5
5
  import { spawn } from 'node:child_process'
@@ -1,6 +1,6 @@
1
1
  // L1 · domain —— runtime.js(从宿主运行时读取信息:loader 条目 / webServer 端口)。
2
2
  // 这两个函数原本吃 cordis 的 ctx;Step 8c 起它们的形参改名为 ports(= 调用方注入的窄接口),domain 层不再出现 ctx 标识符。
3
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
3
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
4
4
 
5
5
  import { FIBER_PHASE } from './compat.js'
6
6
  import { rowIdOf } from '../infra/paths.js'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— skills.js(分层 Step 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, writeFileSync, existsSync, rmSync, readdirSync, mkdirSync } from 'node:fs'
5
5
  import { dirname, join } from 'node:path'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— sources.js(分层 Step 从 lib/index.js 搬出,只搬移未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, rmSync } from 'node:fs'
5
5
  import { writeFile } from 'node:fs/promises'
@@ -1,5 +1,5 @@
1
1
  // L1 · domain —— suite.js(聚合套装:.gitmodules 解析 / 预设目录发现 / 包入口存在性;分层 Step 5 从 lib/index.js 搬出,只搬移未改逻辑。注:runSuiteInstallJob 含 ctx,留到 Step 8)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md 三
2
+ // 分层分组:L1 · domain(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, existsSync, rmSync, readdirSync, mkdirSync } from 'node:fs'
5
5
  import { dirname, join, basename } from 'node:path'
@@ -1,5 +1,5 @@
1
1
  // 由 Step 1 搬运工具从 lib/index.js 原样切出(只移动、未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L0 · infra
2
+ // 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { execFile, spawn, spawnSync } from 'node:child_process'
5
5
  import { promisify } from 'node:util'
@@ -44,7 +44,7 @@ function cmdCommandLine(parts) {
44
44
  .map((text) => {
45
45
  // 2026-09-28 改错(真机:`cmd-corepack` 一条都跑不动 → pnpm-runners / suite-fallback /
46
46
  // install-smoke 三套红,报 `cmd-corepack :: status=1 out=""`):
47
- // `corepack` 在本机是 **cmd 批处理 shim**(`D:\nvm4w\nodejs\corepack.cmd` →
47
+ // `corepack` 在 Windows 上常是 **cmd 批处理 shim**(`<node 安装目录>\corepack.cmd` →
48
48
  // `node …\corepack\dist\corepack.js %*`)。对批处理,cmd **不会剥掉参数外层的引号**,
49
49
  // 引号会随 `%*` 一路进到 node 的 argv —— 于是 corepack 收到的是 `"pnpm"` / `"--version"`
50
50
  // (带引号当成了包管理器名)→ 直接崩:status=1、stdout 空。
@@ -1,5 +1,5 @@
1
1
  // 由 Step 1 搬运工具从 lib/index.js 原样切出(只移动、未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L0 · infra
2
+ // 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { existsSync, rmSync, readdirSync, mkdirSync, copyFileSync, chmodSync, lstatSync, renameSync } from 'node:fs'
5
5
  import { dirname, join, basename } from 'node:path'
@@ -66,9 +66,9 @@ let writeQueue = Promise.resolve()
66
66
 
67
67
  /**
68
68
  * 删除目录树并**核实删除结果**。
69
- * 为什么要核实:本机某些环境(受限令牌/沙箱/杀软占用)下 `rmSync` 会**静默落空**——不抛错、目录仍在。
69
+ * 为什么要核实:某些环境(受限令牌/沙箱/杀软占用)下 `rmSync` 会**静默落空**——不抛错、目录仍在。
70
70
  * 路由若删完直接 `{ok:true}` 就是对用户撒谎(2026-09-20 多类型演练实测:同一个 `rmSync` 在
71
- * `D:\dsh\repos` 删得掉,在 `C:\Users\<user>\.dsh\...` 下返回成功但目录原封不动)。
71
+ * 短路径(如 `C:\repos`)删得掉,在 `C:\Users\<user>\.dsh\...` 下返回成功但目录原封不动)。
72
72
  * 返回 `{ok, attempts, error}`;`ok:false` 时调用方必须如实报错,不能吞。
73
73
  */
74
74
  function removeDirVerified(dir) {
@@ -1,5 +1,5 @@
1
1
  // 由 Step 1 搬运工具从 lib/index.js 原样切出(只移动、未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L0 · infra
2
+ // 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { execFile } from 'node:child_process'
5
5
  import { resolve } from 'node:path'
@@ -1,5 +1,5 @@
1
1
  // 由 Step 1 搬运工具从 lib/index.js 原样切出(只移动、未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L0 · infra
2
+ // 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
 
5
5
  /** 写请求跨站防护:浏览器同源策略不阻止跨站发送,必须校验 Origin / Sec-Fetch-Site。
@@ -1,5 +1,5 @@
1
1
  // 由 Step 1 搬运工具从 lib/index.js 原样切出(只移动、未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L0 · infra
2
+ // 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
 
5
5
  function maskUrl(url) {
@@ -1,5 +1,5 @@
1
1
  // L0 · infra(paths.js)—— 分层 Step 从 lib/index.js 搬出,只搬移未改逻辑
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三
2
+ // 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
  import { readFileSync, existsSync, statSync } from 'node:fs'
5
5
  import { dirname, join, resolve } from 'node:path'
@@ -1,5 +1,5 @@
1
1
  // 由 Step 1 搬运工具从 lib/index.js 原样切出(只移动、未改逻辑)
2
- // 分组见 D:\dsh\dsh-plugin-hub-plan\architecture.zh.md §三 L0 · infra
2
+ // 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
3
3
 
4
4
 
5
5
  /** 最小 semver:解析(含 prerelease/build)。 */
@@ -109,7 +109,7 @@ async function routeRepoLandConfig(req, res, rc) {
109
109
  const body = rc.body
110
110
  const dir = typeof body.dir === 'string' ? body.dir.trim() : ''
111
111
  if (dir === '' || !/^[A-Za-z]:[\\/]/.test(dir) && !/^\\\\/.test(dir)) {
112
- // 0.5.28 改错:示例原本写成本机真实目录 `D:\dsh\repos`(把开发者的目录当成了通用示例)。
112
+ // 0.5.28 改错:示例原本写成本机真实目录(把开发者的目录当成了通用示例)。
113
113
  // 换成中性的任意绝对路径示例,并说明"任意绝对路径"——判据是形状(盘符/UNC),不是具体目录。
114
114
  sendError(res, 400, '保存路径不合法:请提供绝对路径(任意绝对路径均可,如 C:\\repos 或 \\\\server\\share\\repos)')
115
115
  return
@@ -1,7 +1,6 @@
1
1
  // L2 · routes —— 框架升级(POST /framework-upgrade)
2
- // 分层 Step 8b:从 lib/index.js 的 handle() 原样搬出(只搬移未改逻辑)。
3
- // 注:这个 handler 661 行(内置大段 PowerShell 脚本模板),是守卫「单文件 ≤ 600 行」的受限例外(见守卫 SIZE_EXCEPTIONS);
4
- // 待抽包阶段把脚本模板挪进 domain/framework-script.js 后撤销例外。
2
+ // 分层 Step 8b:从 lib/index.js 的 handle() 原样搬出(只搬移未改逻辑)。这个 handler 内置大段
3
+ // PowerShell 脚本模板,是守卫「单文件 ≤ 600 行」的受限例外(见 SIZE_EXCEPTIONS),待抽包阶段撤销。
5
4
 
6
5
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
7
6
  import { writeFile } from 'node:fs/promises'
@@ -11,7 +10,7 @@ import { tmpdir } from 'node:os'
11
10
  import { pathToFileURL } from 'node:url'
12
11
  import { createRequire } from 'node:module'
13
12
  import { readCompatMode, writeCompatMode } from '../domain/compat-state.js'
14
- import { FRAMEWORK_BACKUP_ROOT, backupProfileSnapshot, checkpointFrameworkTree, preflightDisableIncompatible, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, resolveFrameworkUpgradePlan, shellHostedRefusal } from '../domain/framework.js'
13
+ import { FRAMEWORK_BACKUP_ROOT, backupProfileSnapshot, checkpointFrameworkTree, platformUpgradeRefusal, preflightDisableIncompatible, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, resolveFrameworkUpgradePlan, shellHostedRefusal } from '../domain/framework.js'
15
14
  import { fwIntegrityCheck } from '../infra/fw-integrity-check.js'
16
15
  import { CORE_PATCH_ROW_IDS } from '../domain/patch.js'
17
16
  import { migrateAgentConfigsForUpgrade } from '../domain/presets.js'
@@ -80,12 +79,15 @@ async function routeFrameworkUpgrade(req, res, rc) {
80
79
  } catch {}
81
80
  }
82
81
  // 拉元数据 → 候选版本列表 → 本次目标(helper 里同时做「用户自选版本」的白名单校验)
83
- const plan = await resolveFrameworkUpgradePlan({ current, wanted: body.version, fetchJson: fetchJsonUrl })
82
+ const plan = await resolveFrameworkUpgradePlan({ current, wanted: body.version, fetchJson: rc.deps.fetchJson ?? fetchJsonUrl })
84
83
  const { latest, next, alpha, versions, tagDefault, registryError } = plan
85
84
  if (plan.rejected !== null) {
86
85
  sendError(res, 400, `升级目标 ${plan.rejected} 不在可选列表内(只接受比当前 ${current ?? '?'} 更新的已发布版本;降级请用「回滚到上一版」)`)
87
86
  return
88
87
  }
88
+ // 平台守卫(2026-09-29 改错):本通道 = schtasks.exe + PowerShell,只有 win32 有;判据与文案见
89
+ // domain/framework.js#platformUpgradeRefusal。放在入参校验之后、任何副作用之前 —— 绝不假成功。
90
+ const platformRefusal = platformUpgradeRefusal(); if (platformRefusal !== null) { sendError(res, 501, platformRefusal.error, platformRefusal.details); return }
89
91
  const target = plan.target
90
92
  const profileDir = dirname(findPatchPath(ctx))
91
93
  // 1) 升级前打包备份现有配置(patch / profile package.json / 插件清单)
@@ -138,17 +140,16 @@ async function routeFrameworkUpgrade(req, res, rc) {
138
140
  steps.push(`另有 ${preflight.skipped.length} 行无法预判(受保护行/包信息读不到),保持启用,由升级后的启动失败隔离兜底`)
139
141
  }
140
142
  }
141
- // 2) 自动升级框架:运行中的服务锁着 dsh/lib 目录(EBUSY 无法原地替换),
142
- // 因此生成独立升级脚本(kill 服务 → npm 升级 → 失败回滚 → 拉起服务),后台执行。
143
- // 与 /restart 同款自守护模式:端口无监听会自动拉起,不会留下"服务起不来"。
143
+ // 2) 自动升级框架:运行中的服务锁着 dsh/lib 目录(EBUSY 无法原地替换)→ 生成独立升级脚本
144
+ // (kill 服务 → npm 升级 → 失败回滚 → 拉起服务)后台执行;与 /restart 同款自守护,不会留下"服务起不来"。
144
145
  if (hasUpdate && dshDir !== null) {
145
146
  const port = webPort(ctx)
146
147
  const nodePath = process.execPath
147
148
  const binPath = resolveDshBin()
148
149
  const npmCli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
149
150
  // 事故教训:npm 的 lib/cli.js 是「函数模块」(module.exports = (process) => validateEngines(...)),
150
- // 直跑 `node lib/cli.js` 只加载函数定义、不执行命令——立即退出 exit 0(假成功,曾导致
151
- // "npm 退出码 0 但版本未更新")。唯一正确入口是 bin/npm-cli.js(它调用 cli.js 函数)。
151
+ // 直跑它只加载函数定义、不执行命令——立即退出 exit 0(假成功,"npm 退出码 0 但版本未更新")。
152
+ // 唯一正确入口是 bin/npm-cli.js(它调用 cli.js 函数)。
152
153
  const corepackJs = join(dirname(process.execPath), 'node_modules', 'corepack', 'dist', 'corepack.js')
153
154
  // 预检(事故教训):corepack(pnpm 通道)/ dsh bin.js 缺失时**拒绝生成升级脚本**(不 kill 服务)——
154
155
  // 避免"kill 后升级/拉起双双失败,服务永久 down"
@@ -640,16 +641,14 @@ async function routeFrameworkUpgrade(req, res, rc) {
640
641
  setFwCheckCache(null)
641
642
  // UTF-8 BOM:powershell 5.1 按 ANSI 读无 BOM 文件,中文会乱码——加 BOM 保证解析正确。
642
643
  // 执行方式:schtasks 一次性计划任务(Task Scheduler 启动,独立于服务进程树/Job Object)——
643
- // 服务进程被杀时,其子进程(execFile/detached 的 powershell)会被 Job/进程树连带杀死,
644
- // 曾多次卡死在「停止服务」之后;计划任务彻底脱离,杀服务绝对影响不到升级脚本。
644
+ // 服务进程被杀时其子进程(execFile/detached 的 powershell)会被 Job 连带杀死,
645
+ // 计划任务彻底脱离,杀服务影响不到升级脚本。
645
646
  writeFile(ps1, `\uFEFF${lines}`, 'utf8').then(
646
647
  () => {
647
648
  const ps1Posix = ps1.replace(/\\/gu, '/')
648
- // 无引号 /tr(重要):Task Scheduler 对带引号命令的解析会把 Command 拆坏成
649
- // `"powershell ... -File \"`(非有效可执行文件)——任务显示 Ready、/run 报 SUCCESS
650
- // 但永不执行(曾导致升级/重启脚本反复"已启动"却不动作、服务不停止)。实测无引号
651
- // 格式(exe 与脚本路径均无空格时)任务正常执行、脚本完整跑通。仅当脚本路径含空格
652
- // 时才退回带引号格式(schtasks 引号解析在服务 execFile 上下文不可靠,此时宁可用它)。
649
+ // 无引号 /tr(重要):Task Scheduler 会把带引号命令的 Command 拆坏成 `"powershell … -File \"`
650
+ // (非有效可执行文件)——任务显示 Ready、/run 报 SUCCESS 但永不执行(曾导致脚本反复"已启动"
651
+ // 却不动作)。实测无引号格式(exe 与脚本路径均无空格时)正常;仅脚本路径含空格时才退回带引号格式。
653
652
  const tr = / /.test(ps1Posix)
654
653
  ? `"powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \\"${ps1Posix}\\""`
655
654
  : `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ${ps1Posix}`
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from 'node:path'
8
8
  import { tmpdir } from 'node:os'
9
9
  import { createRequire } from 'node:module'
10
10
  import { readCompatGate, writeCompatGate } from '../domain/compat.js'
11
- import { cleanupStaleFwTasks, currentFrameworkVersion, pickFrameworkTarget, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, shellHostedRefusal } from '../domain/framework.js'
11
+ import { cleanupStaleFwTasks, currentFrameworkVersion, pickFrameworkTarget, platformUpgradeRefusal, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, shellHostedRefusal } from '../domain/framework.js'
12
12
  import { readGithubAuth } from '../domain/install.js'
13
13
  import { webPort } from '../domain/runtime.js'
14
14
  import { fetchJsonUrl } from '../infra/http.js'
@@ -32,6 +32,21 @@ function refuseWhenShellHosted(res) {
32
32
  return true
33
33
  }
34
34
 
35
+ /**
36
+ * 平台守卫(2026-09-29 改错,与宿主守卫同形):一键升级/回滚脚本整体依赖 Windows 机制 ——
37
+ * `schtasks.exe` 计划任务 + PowerShell + `Get-NetTCPConnection`;非 Windows 上 execFile 必然 ENOENT,
38
+ * 而旧实现会先写一个「脚本已启动」的状态文件、再去 exec 不存在的 powershell.exe,**最后仍回 ok:true**
39
+ * (用户以为在跑,实际零动作)。判据与文案见 domain/framework.js#platformUpgradeRefusal。
40
+ * 命中时:不写状态文件、不 spawn、不建计划任务,直接 501 + 手动升级出路。
41
+ * 返回 true = 已拒绝(调用方直接 return);false = win32,原路径一字不改。
42
+ */
43
+ function refuseWhenPlatformUnsupported(res) {
44
+ const refusal = platformUpgradeRefusal()
45
+ if (refusal === null) return false
46
+ sendError(res, 501, refusal.error, refusal.details)
47
+ return true
48
+ }
49
+
35
50
  async function routeFrameworkUpgradeStatusGet(req, res, rc) {
36
51
  const ctx = rc.ctx
37
52
  const url = rc.url
@@ -355,6 +370,7 @@ async function routeFrameworkRollback(req, res, rc) {
355
370
  sendError(res, 409, '没有可用的框架全树回滚点(framework-rollback.json 缺失或 checkpoint 已清理)')
356
371
  return
357
372
  }
373
+ if (refuseWhenPlatformUnsupported(res)) return // 平台守卫(改错):回滚脚本同样是 schtasks+PowerShell,非 win32 一律拒绝而不是假成功(放在只读状态校验之后、任何写盘/exec 之前)
358
374
  const port = webPort(ctx)
359
375
  const nodePath = process.execPath
360
376
  const taskName = `DSH-FW-Rollback-${process.pid}`
@@ -576,4 +592,4 @@ async function routeFrameworkStatusClear(req, res, rc) {
576
592
  }
577
593
  sendJson(res, 200, { ok: true, removed, status: 'idle', message: null })
578
594
  }
579
- export { refuseWhenShellHosted, routeFrameworkStatusClear, routeFrameworkUpgradeStatusGet, routeFrameworkRelaunch, routeFrameworkCheck, routeCompatGate, routeCheckUpdate, routeFrameworkRollback, routeRestart }
595
+ export { refuseWhenShellHosted, refuseWhenPlatformUnsupported, routeFrameworkStatusClear, routeFrameworkUpgradeStatusGet, routeFrameworkRelaunch, routeFrameworkCheck, routeCompatGate, routeCheckUpdate, routeFrameworkRollback, routeRestart }
@@ -149,8 +149,8 @@ function shortOpenError(error) {
149
149
  * POST /plugin-console/github-open-login —— 唤起 dsh-github-login 的设备码登录窗口。
150
150
  *
151
151
  * 为什么失败也回 200(而不是 500):这个能力是**外挂**的,装没装、装的是不是带 exe 的版本、
152
- * 当前 profile 有没有把它挂到 webServer 上,全都不由本插件决定(2026-09-20 实测:本机虽然装了
153
- * 该插件、exe 也在 D:\dsh\dsh-github-login\dist\,但当前 profile 下 GET /github-auth/status 仍是
152
+ * 当前 profile 有没有把它挂到 webServer 上,全都不由本插件决定(2026-09-20 实测:某个 profile 里虽然装了
153
+ * 该插件、exe 也在,但当前 profile 下 GET /github-auth/status 仍是
154
154
  * 404)。这些都是"这个通道现在不可用",不是"服务端出错":
155
155
  * · 回 500 → 前端弹一句看不懂的报错,还看不出"该走 token 兜底"这条正路;
156
156
  * · 回 200 + started:false + reason → 前端照原样把原因念给用户,并自动展开 token 粘贴面板。
@@ -11,6 +11,7 @@ import { pnpmRemove, readExtraBundleRows, runInstallJob } from '../domain/instal
11
11
  import { isProtectedModule, listEntries, webPort } from '../domain/runtime.js'
12
12
  import { DEFAULT_SEARCH } from '../domain/sources.js'
13
13
  import { runSuiteInstallJob } from '../domain/suite.js'
14
+ import { fetchJsonUrl } from '../infra/http.js'
14
15
  import { readBody, sendError } from '../infra/httpd.js'
15
16
  import { routeAiConsent, routeAiEmpowerCancel, routeAiEmpowerList, routeAiEmpowerPlan, routeAiEmpowerRun, routeAiEmpowerStatus } from './ai.js'
16
17
  import { routeRunSuggested } from './actions.js'
@@ -53,7 +54,9 @@ const ROUTES_EARLY = [
53
54
  ]
54
55
 
55
56
  function routeDeps() {
56
- return { runInstallJob, runSuiteInstallJob, webPort, readExtraBundleRows, detectAdoptablePending, listEntries, detectCompat, detectFrameworkUpgrade, DEFAULT_SEARCH, aiEmpowerExecute, aiEmpowerPlan, backupProfileSnapshot, currentFrameworkVersion, frameworkCompatReportFor, isProtectedModule, pnpmRemove, preflightDisableIncompatible, rowIdModuleMap }
57
+ // fetchJson:registry 元数据取数的注入缝(默认就是真通道)。测试要断言「非 win32 拒绝且零副作用」
58
+ // 时用它喂固定元数据,免得把网络与 CI 稳定性绑在一起(与其余 deps 同一种注入风格)。
59
+ return { runInstallJob, runSuiteInstallJob, webPort, readExtraBundleRows, detectAdoptablePending, listEntries, detectCompat, detectFrameworkUpgrade, DEFAULT_SEARCH, aiEmpowerExecute, aiEmpowerPlan, backupProfileSnapshot, currentFrameworkVersion, frameworkCompatReportFor, isProtectedModule, pnpmRemove, preflightDisableIncompatible, rowIdModuleMap, fetchJson: fetchJsonUrl }
57
60
  }
58
61
 
59
62
  async function handle(ctx, req, res) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.5.30",
3
+ "version": "0.5.31",
4
4
  "description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
5
5
  "repository": {
6
6
  "type": "git",