dsh-plugin 2.0.6 → 2.2.0
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.en.md +11 -11
- package/README.md +11 -11
- package/client/client.js +585 -328
- package/client/client.js.map +1 -1
- package/lib/http/diagnostics.js +2 -63
- package/lib/http/install.js +42 -11
- package/lib/http/settings.js +1 -1
- package/lib/services/host-version.js +72 -0
- package/lib/services/install/decode-output.js +23 -0
- package/lib/services/install/dsh-compat.js +105 -0
- package/lib/services/install/operation-log.js +61 -11
- package/lib/services/install/registry-fallback.js +89 -0
- package/lib/services/install/task-queue.js +95 -19
- package/lib/services/settings.js +1 -0
- package/lib/types/services/host-version.d.ts +10 -0
- package/lib/types/services/install/decode-output.d.ts +4 -0
- package/lib/types/services/install/dsh-compat.d.ts +19 -0
- package/lib/types/services/install/install-types.d.ts +4 -0
- package/lib/types/services/install/operation-log.d.ts +10 -4
- package/lib/types/services/install/registry-fallback.d.ts +18 -0
- package/lib/types/services/install/task-queue.d.ts +18 -0
- package/lib/types/services/settings.d.ts +3 -1
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +11 -2
- package/src/client/components/modals/NotificationsModal.tsx +62 -5
- package/src/client/components/modals/modals.tsx +68 -5
- package/src/client/components/views/SettingsView.tsx +10 -0
- package/src/client/hooks/useCatalog.ts +10 -8
- package/src/client/hooks/useSettings.ts +4 -1
- package/src/client/hooks/useTaskQueue.ts +7 -1
- package/src/client/locales.ts +42 -0
- package/src/client/logic/failures.ts +86 -5
- package/src/client/logic/release-age.ts +40 -0
- package/src/client/logic/urls.ts +19 -4
- package/src/client/types.ts +13 -0
- package/src/server/http/diagnostics.ts +2 -59
- package/src/server/http/install.ts +46 -11
- package/src/server/http/settings.ts +1 -1
- package/src/server/services/host-version.ts +69 -0
- package/src/server/services/install/decode-output.ts +22 -0
- package/src/server/services/install/dsh-compat.ts +99 -0
- package/src/server/services/install/install-types.ts +4 -0
- package/src/server/services/install/operation-log.ts +58 -11
- package/src/server/services/install/registry-fallback.ts +89 -0
- package/src/server/services/install/task-queue.ts +107 -19
- package/src/server/services/settings.ts +4 -1
|
@@ -14,13 +14,16 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
14
14
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.js';
|
|
15
15
|
import { npmTooLowMarker } from './npm-check.js';
|
|
16
16
|
import { registryMissingMarker } from './registry-check.js';
|
|
17
|
-
import { backfillOperationLog, createOperationLogTap, operationLogPathOf } from './operation-log.js';
|
|
17
|
+
import { backfillOperationLog, createOperationLogTap, hasSubstantiveOutput, operationLogPathOf } from './operation-log.js';
|
|
18
18
|
import { cleanLine, estimateProgress } from '../profile/progress.js';
|
|
19
19
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
|
|
20
20
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.js';
|
|
21
21
|
import { addAllowBuildsKey, bundleIncludes, githubRepoOf, parseAllowBuildsKey, profileDirectory } from '../profile/profile.js';
|
|
22
22
|
import { appendLog } from '../log.js';
|
|
23
23
|
import { commandOnPath, gitMissingDetail } from './preflight.js';
|
|
24
|
+
import { isTransportFailure, registryHost } from './registry-fallback.js';
|
|
25
|
+
import { dshTooOldMarker } from './dsh-compat.js';
|
|
26
|
+
import { OutputDecoder } from './decode-output.js';
|
|
24
27
|
/** 内存任务注册表,按任务 id 索引。 */
|
|
25
28
|
const tasks = new Map();
|
|
26
29
|
let nextTaskId = 1;
|
|
@@ -195,6 +198,17 @@ export function desktopCliBootstrap(entry) {
|
|
|
195
198
|
}
|
|
196
199
|
return null;
|
|
197
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
|
|
203
|
+
* 其次更新走 `@latest`,git 源与全局 npm 安装保持原样。
|
|
204
|
+
*/
|
|
205
|
+
export function npmInstallTargetOf(target, opts) {
|
|
206
|
+
if (opts.global || githubRepoOf(target) !== null)
|
|
207
|
+
return target;
|
|
208
|
+
if (opts.pinned !== undefined && opts.pinned !== '')
|
|
209
|
+
return opts.pinned;
|
|
210
|
+
return opts.update ? `${target}@latest` : target;
|
|
211
|
+
}
|
|
198
212
|
function stopChild(child) {
|
|
199
213
|
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
200
214
|
const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
|
|
@@ -346,12 +360,18 @@ function pumpQueue() {
|
|
|
346
360
|
* 跟踪的任务上,并以捕获结果 resolve。绝不 reject;失败通过结果/任务状态体现。
|
|
347
361
|
*/
|
|
348
362
|
function spawnMutation(options) {
|
|
349
|
-
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, loader, globalNpm, updateNpm } = options;
|
|
363
|
+
const { action, profile, target, timeoutMs = 5 * 60 * 1000, env, task, displayTarget, loader, globalNpm, updateNpm, registries, installSpec } = options;
|
|
350
364
|
// 全局 npm 安装:直接 spawn 本机 npm(`npm install -g <pkgs>`),不是 dsh plugin 命令
|
|
351
365
|
const isGlobalNpm = (globalNpm ?? []).length > 0;
|
|
352
366
|
const invocation = isGlobalNpm
|
|
353
367
|
? { file: 'npm', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' }
|
|
354
368
|
: cliInvocation(profile);
|
|
369
|
+
// 打包桌面端却回退到 PATH 上的 dsh:说明 desktop CLI 引导路径没命中,desktop profile 必被宿主拒绝。
|
|
370
|
+
// 留一行 ASCII 标记,后续同类上报可直接区分「用户自起 CLI」与「我们没走对引导路径」。
|
|
371
|
+
if (task && task.status !== 'cancelled' && invocation.file === 'dsh'
|
|
372
|
+
&& process.versions.electron !== undefined) {
|
|
373
|
+
pushLine(task, '[desktop-cli-fallback] running inside Electron but no desktop CLI bootstrap was found — falling back to dsh on PATH (a desktop-managed profile will be rejected)');
|
|
374
|
+
}
|
|
355
375
|
// 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
|
|
356
376
|
// (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
|
|
357
377
|
// 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
|
|
@@ -359,9 +379,8 @@ function spawnMutation(options) {
|
|
|
359
379
|
// CLI 动词仍透传 add —— 不执行官方 update 动词,避免与既有覆盖重装机制分叉。
|
|
360
380
|
const isUpdate = action === 'update' || updateNpm === true;
|
|
361
381
|
const cliAction = action === 'update' ? 'add' : action;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
: target;
|
|
382
|
+
// 钉住版本优先于 @latest:提示的是「已过门禁」的版本,装 latest 会被 pnpm 拒绝
|
|
383
|
+
const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec });
|
|
365
384
|
const args = isGlobalNpm
|
|
366
385
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
367
386
|
: [...invocation.prefixArgs, 'plugin', '--profile', profile, cliAction, npmUpdateTarget];
|
|
@@ -375,9 +394,12 @@ function spawnMutation(options) {
|
|
|
375
394
|
// 单次 spawn 并捕获结果:流式输出打日志 + 超时/取消兜底,close 后 resolve。
|
|
376
395
|
// 不做任何终态后处理(exitCode/status/needsRestart 由下方统一收尾),
|
|
377
396
|
// 这样全局 npm 首次失败后可以干净地换参数重试一次。
|
|
378
|
-
const spawnOnce = (attemptArgs) => new Promise((resolveOnce) => {
|
|
397
|
+
const spawnOnce = (attemptArgs, registry) => new Promise((resolveOnce) => {
|
|
379
398
|
let stdout = '';
|
|
380
399
|
let stderr = '';
|
|
400
|
+
// 每次 spawn 独立解码器:中文 Windows 下 dsh/pnpm 可能输出 GBK,需按流判定编码
|
|
401
|
+
const stdoutDecoder = new OutputDecoder();
|
|
402
|
+
const stderrDecoder = new OutputDecoder();
|
|
381
403
|
let timedOut = false;
|
|
382
404
|
let settled = false;
|
|
383
405
|
let child;
|
|
@@ -411,11 +433,17 @@ function spawnMutation(options) {
|
|
|
411
433
|
return;
|
|
412
434
|
}
|
|
413
435
|
}
|
|
436
|
+
// invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):
|
|
437
|
+
// 没有它时保持 env 原样透传,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
|
|
438
|
+
// registry 最后写入:换源重试只改这一层,pnpm/npm 都认 npm_config_registry。
|
|
439
|
+
const childEnv = { ...(env ?? process.env), ...(invocation.env ?? {}) };
|
|
440
|
+
if (registry !== undefined) {
|
|
441
|
+
childEnv.npm_config_registry = registry;
|
|
442
|
+
childEnv.NPM_CONFIG_REGISTRY = registry;
|
|
443
|
+
}
|
|
414
444
|
child = spawn(invocation.file, attemptArgs, {
|
|
415
445
|
cwd: invocation.cwd,
|
|
416
|
-
|
|
417
|
-
// 没有它时保持 env 原样透传,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
|
|
418
|
-
env: invocation.env === undefined ? env : { ...(env ?? process.env), ...invocation.env },
|
|
446
|
+
env: childEnv,
|
|
419
447
|
shell: invocation.useShell,
|
|
420
448
|
// detached 使子进程成为独立进程组组长:取消时能整组清理(见 stopChild),
|
|
421
449
|
// 且宿主进程崩溃后安装任务不会被连带误杀
|
|
@@ -477,7 +505,7 @@ function spawnMutation(options) {
|
|
|
477
505
|
task.progress += 1;
|
|
478
506
|
}, 500);
|
|
479
507
|
const collect = (kind, chunk) => {
|
|
480
|
-
const text = chunk.
|
|
508
|
+
const text = kind === 'stdout' ? stdoutDecoder.push(chunk) : stderrDecoder.push(chunk);
|
|
481
509
|
if (kind === 'stdout')
|
|
482
510
|
stdout = (stdout + text).slice(-CAPTURE_LIMIT_BYTES);
|
|
483
511
|
else
|
|
@@ -536,7 +564,23 @@ function spawnMutation(options) {
|
|
|
536
564
|
// 超时复核用基线:本次任务开始前目标是否已在 profile 里。已安装的话,超时后核对无法区分
|
|
537
565
|
// 「旧版本还在」与「刚装上」,那种情形(更新超时)不冒认成功。
|
|
538
566
|
const wasInstalled = action !== 'remove' && !isGlobalNpm && verifyInstalledEntry(profile, target).name !== null;
|
|
539
|
-
|
|
567
|
+
// 候选源:卸载不拉包、git 源装的是仓库快照,都不受 npm 镜像影响,不参与换源
|
|
568
|
+
const candidates = action === 'remove' || githubRepoOf(target) !== null ? [] : (registries ?? []);
|
|
569
|
+
let activeRegistry = candidates[0];
|
|
570
|
+
let result = await spawnOnce(args, activeRegistry);
|
|
571
|
+
// 传输层失败(超时 / 连接重置 / DNS / 5xx)才换下一个公共镜像;404、版本不存在、鉴权错误
|
|
572
|
+
// 保持原样,否则会掩盖作者侧问题或装到滞后版本。整段超时也不再换源,避免耗时翻倍。
|
|
573
|
+
for (const candidate of candidates.slice(1)) {
|
|
574
|
+
if (task?.status === 'cancelled' || result.exitCode === 0 || result.timedOut)
|
|
575
|
+
break;
|
|
576
|
+
if (!isTransportFailure(`${result.stderr}\n${result.stdout}`))
|
|
577
|
+
break;
|
|
578
|
+
if (task) {
|
|
579
|
+
pushLine(task, `[registry-fallback] ${registryHost(activeRegistry ?? '')} unreachable — retrying with ${registryHost(candidate)}`);
|
|
580
|
+
}
|
|
581
|
+
activeRegistry = candidate;
|
|
582
|
+
result = await spawnOnce(args, candidate);
|
|
583
|
+
}
|
|
540
584
|
// npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`)
|
|
541
585
|
// 会让 `npm install -g` 在解析阶段直接崩溃。首次失败后自动带 `--legacy-peer-deps` 重试一次:
|
|
542
586
|
// 跳过 peer 自动解析即可绕过该 bug;二次尝试仍失败则保留其错误信息,不掩盖真正的原因。
|
|
@@ -547,7 +591,7 @@ function spawnMutation(options) {
|
|
|
547
591
|
task.attempts.push(retryExecuted);
|
|
548
592
|
if (task)
|
|
549
593
|
pushLine(task, '[npm] peer-set resolution crashed npm (known npm bug) — retrying with --legacy-peer-deps');
|
|
550
|
-
result = await spawnOnce(retryArgs);
|
|
594
|
+
result = await spawnOnce(retryArgs, activeRegistry);
|
|
551
595
|
}
|
|
552
596
|
// 超时被强杀 ≠ 没装上:命中的安装可能已经落盘(日志里 pnpm 已打印 Done,只是 dsh 进程迟迟不退出
|
|
553
597
|
// 而被我们超时掐掉)。判失败之前先复核 profile —— 目标已在依赖里、且声明分发的入口文件都在,
|
|
@@ -611,7 +655,9 @@ function spawnMutation(options) {
|
|
|
611
655
|
addPendingRestart(displayTarget ?? target, action === 'remove' ? 'uninstall' : 'install');
|
|
612
656
|
}
|
|
613
657
|
}
|
|
614
|
-
|
|
658
|
+
// 子进程一个字节都没吐时在退出行上标注:日志面板与失败上报都能看出「没有输出可排查」
|
|
659
|
+
const printedNothing = `${result.stdout}${result.stderr}`.trim() === '';
|
|
660
|
+
pushLine(task, result.timedOut ? '[timed out]' : `[exit ${result.exitCode ?? '?'}]${printedNothing ? ' (no output captured)' : ''}`);
|
|
615
661
|
}
|
|
616
662
|
}
|
|
617
663
|
return result;
|
|
@@ -707,6 +753,23 @@ async function annotateRegistryMissing(result, options) {
|
|
|
707
753
|
pushLine(options.task, line);
|
|
708
754
|
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
709
755
|
}
|
|
756
|
+
/** 安装失败且插件声明的最低 DSH 版本高于当前宿主 → 注入 [dsh-too-old] 标记。
|
|
757
|
+
* 只在失败路径上发生:peer 不满足本身通常只是警告,装成功时不该冒这个提示。
|
|
758
|
+
* 超时也算失败 —— 旧宿主上插件可能卡在半兼容状态,这个信息同样有用。 */
|
|
759
|
+
async function annotateDshTooOld(result, options) {
|
|
760
|
+
if (options.action === 'remove' || result.exitCode === 0)
|
|
761
|
+
return result;
|
|
762
|
+
// 全局 npm 装的是本机 CLI 工具,不进 profile,没有宿主版本要求
|
|
763
|
+
if ((options.globalNpm ?? []).length > 0)
|
|
764
|
+
return result;
|
|
765
|
+
// 用安装实际使用的源读元数据:私有源/镜像里也能读到同一个包的声明
|
|
766
|
+
const line = await dshTooOldMarker(options.target, { registry: options.registries?.[0] });
|
|
767
|
+
if (line === null)
|
|
768
|
+
return result;
|
|
769
|
+
if (options.task && options.task.status !== 'cancelled')
|
|
770
|
+
pushLine(options.task, line);
|
|
771
|
+
return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line };
|
|
772
|
+
}
|
|
710
773
|
/**
|
|
711
774
|
* 宿主自身 CLI 调不起来时的 ASCII 标记行:spawn 我们解析出的宿主入口报 ENOENT 时返回
|
|
712
775
|
* `[host-spawn]`,供前端单独归因、不引导提 Issue。PATH 上的 dsh/npm 各有自己的标记,返回 null。
|
|
@@ -728,7 +791,8 @@ export function hostSpawnMarker(invocation, error) {
|
|
|
728
791
|
*/
|
|
729
792
|
export async function runPluginMutation(options) {
|
|
730
793
|
// operation 日志只在运行期存在(宿主结束后可能清掉),先开抓取,退出后按路径读作兜底
|
|
731
|
-
const
|
|
794
|
+
const profileDir = profileDirectory(options.profile);
|
|
795
|
+
const logTap = createOperationLogTap(profileDir);
|
|
732
796
|
let result = await spawnMutation(options);
|
|
733
797
|
// allowBuilds 放行 / 装后入口校验都是 profile 专属(pnpm 装进 profile 的场景);
|
|
734
798
|
// 全局 npm 安装直接走 npm,无 pnpm allowBuilds 拦截、也不进 profile,跳过这两类恢复
|
|
@@ -791,9 +855,10 @@ export async function runPluginMutation(options) {
|
|
|
791
855
|
// 宿主只把一行摘要(`dsh: plugin command failed; diagnostics: <path>`)回显到控制台、真正的报错
|
|
792
856
|
// 写进该日志文件时,前端拿不到任何错误特征、只能归为「未知」。按该路径回捞日志尾部补进失败信息。
|
|
793
857
|
// 只在输出本就不含错误码时回捞 —— pnpm 已回显报错时无需多读一次文件。
|
|
794
|
-
|
|
858
|
+
// 超时同样回捞:被强杀前 pnpm 往往已经把真实原因写进 operation 日志。
|
|
859
|
+
if ((result.exitCode !== 0 || result.timedOut) && !/\bERR_[A-Z0-9_]+\b/.test(`${result.stderr}\n${result.stdout}`)) {
|
|
795
860
|
const output = `${result.stderr}\n${result.stdout}`;
|
|
796
|
-
const backfilled = tappedLog !== '' ? tappedLog : backfillOperationLog(output);
|
|
861
|
+
const backfilled = tappedLog !== '' ? tappedLog : backfillOperationLog(output, profileDir);
|
|
797
862
|
if (backfilled !== null) {
|
|
798
863
|
if (options.task && options.task.status !== 'cancelled') {
|
|
799
864
|
pushLine(options.task, '[pnpm log] DSH did not echo pnpm output — recovered the operation log from disk');
|
|
@@ -803,13 +868,24 @@ export async function runPluginMutation(options) {
|
|
|
803
868
|
}
|
|
804
869
|
result = { ...result, stderr: `${result.stderr}\n${backfilled}` };
|
|
805
870
|
}
|
|
806
|
-
else if (options.task && options.task.status !== 'cancelled'
|
|
807
|
-
|
|
808
|
-
|
|
871
|
+
else if (options.task && options.task.status !== 'cancelled') {
|
|
872
|
+
if (operationLogPathOf(output) !== null) {
|
|
873
|
+
// 诊断路径在、日志却取不到:留一行证据,否则下一次同类上报又只剩宿主那句摘要、看不出根因
|
|
874
|
+
pushLine(options.task, '[pnpm log] the host-reported diagnostics log could not be read from disk (missing, empty, or unreadable)');
|
|
875
|
+
}
|
|
876
|
+
else if (!hasSubstantiveOutput(output)) {
|
|
877
|
+
// 宿主连 diagnostics 路径都没回显:显式留痕,避免上报正文只剩一个退出码
|
|
878
|
+
const what = result.timedOut
|
|
879
|
+
? 'printed nothing before the timeout'
|
|
880
|
+
: `exited with code ${result.exitCode ?? '?'} without printing anything`;
|
|
881
|
+
pushLine(options.task, `[no-output] the dsh CLI ${what}, and no pnpm log was written under .plugin-manager/logs — attach ~/.dsh/profiles/${options.profile}/hub.log when reporting this`);
|
|
882
|
+
}
|
|
809
883
|
}
|
|
810
884
|
}
|
|
811
885
|
// 包在 registry 不存在(404)→ 官方源复查,区分「作者的依赖缺失/包被删」与「本机镜像源滞后」
|
|
812
886
|
result = await annotateRegistryMissing(result, options);
|
|
887
|
+
// 插件声明的最低 DSH 版本高于当前宿主 → 追加 [dsh-too-old] 标记,前端提示先升级 DSH
|
|
888
|
+
result = await annotateDshTooOld(result, options);
|
|
813
889
|
// npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
|
|
814
890
|
return annotateNpmTooLow(result, options);
|
|
815
891
|
}
|
package/lib/services/settings.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 从命令输出里抽出版本号本体:pnpm/npm 直接打印 `11.7.0`,而 `git --version` 打印
|
|
2
|
+
* `git version 2.55.0.windows.5` —— 原样取首行会让 issue 正文拼成 `vgit version …`。
|
|
3
|
+
* 只取版本号,顺带兼容 `v1.2.3` 这类自带前缀的输出。 */
|
|
4
|
+
export declare function versionOf(output: string): string | null;
|
|
5
|
+
/** 探测命令行工具版本:未安装 / 不在 PATH / 超时一律 null。
|
|
6
|
+
* Windows 上 pnpm/npm 是 .cmd shim,不走 shell 起不来;参数是固定的 `--version`,没有注入面。 */
|
|
7
|
+
export declare function toolVersion(cmd: string, args: string[]): string | null;
|
|
8
|
+
/** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
|
|
9
|
+
* (dsh 不在 PATH 时兜底)。探测成功即缓存,失败不缓存以便下次重试。 */
|
|
10
|
+
export declare function hostDshVersion(): string | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** 从安装目标里取 npm 包名:`@scope/pkg@1.2.3` → `@scope/pkg`。
|
|
2
|
+
* git / URL / 本地路径等非 npm 目标返回 null。 */
|
|
3
|
+
export declare function npmNameOf(target: string): string | null;
|
|
4
|
+
/** `0.2.1-rc.1` / `^0.2.1` / `>=0.2.0 <0.3.0` → [0,2,1];解析不出返回 null。
|
|
5
|
+
* 只取版本三段:DSH 的 prerelease 序号(rc.1)不参与「太低」判断,避免比较误差。 */
|
|
6
|
+
export declare function versionTriple(raw: string): [number, number, number] | null;
|
|
7
|
+
/** 插件声明的最低 DSH 版本:engines.dsh 与 peerDependencies 里 @deepseek-ai/dsh-* 的下限取最大。
|
|
8
|
+
* 返回原始声明文本(如 `^0.2.1-rc.1`)供提示直接展示;没有声明返回 null。 */
|
|
9
|
+
export declare function dshFloorOf(manifest: unknown): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* 产出「插件要求更高的 DSH」标记行;没有声明、读不到元数据、宿主版本未知或已满足时返回 null。
|
|
12
|
+
* 用版本级 manifest(`/<pkg>/latest`):packument 的精简形态不带 peerDependencies。
|
|
13
|
+
*/
|
|
14
|
+
export declare function dshTooOldMarker(target: string, opts?: {
|
|
15
|
+
currentVersion?: string | null;
|
|
16
|
+
fetchImpl?: typeof fetch;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
registry?: string;
|
|
19
|
+
}): Promise<string | null>;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 从 DSH 输出里解析 operation 日志路径(`diagnostics: <path>`)。
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 去掉首尾引号与句末标点(不同版本/语言的摘要行尾部可能带 `。`、`.`、`)`);
|
|
4
|
+
* 相对路径原样返回,由读取方按 profile 目录解析。
|
|
5
5
|
*/
|
|
6
6
|
export declare function operationLogPathOf(output: string): string | null;
|
|
7
7
|
/**
|
|
8
8
|
* 回捞 operation 日志尾部,作为失败详情返回;拿不到(无路径 / 文件不存在 / 读失败 /
|
|
9
|
-
*
|
|
9
|
+
* 内容为空 / 路径越界)返回 null。尾部截到与子进程输出相同的上限。
|
|
10
10
|
*/
|
|
11
|
-
export declare function backfillOperationLog(output: string): string | null;
|
|
11
|
+
export declare function backfillOperationLog(output: string, profileDir?: string): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* 输出里是否有 dsh 自己的内容 —— 只有 `[exit N]`、`[install-channel]`、`[timed out]`
|
|
14
|
+
* 这类本队列自己打的标记时返回 false。用于把「宿主什么都没回显」的失败显式留痕,
|
|
15
|
+
* 否则上报正文只剩退出码,无从排查。
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasSubstantiveOutput(output: string): boolean;
|
|
12
18
|
/**
|
|
13
19
|
* 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
|
|
14
20
|
* 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** 去掉尾部斜杠,避免同一源因写法不同被当成两个候选。 */
|
|
2
|
+
export declare function normalizeRegistry(value: string): string;
|
|
3
|
+
/** 是否属于允许自动切换的公共源白名单。 */
|
|
4
|
+
export declare function isPublicRegistry(value: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
|
|
7
|
+
* 只有公共源才追加后备(最多 3 个);关闭降级或源不在白名单时只返回它自己。
|
|
8
|
+
*/
|
|
9
|
+
export declare function registryCandidates(configured: string, userRegistry: string, enabled: boolean): string[];
|
|
10
|
+
/** 本机 npm 实际生效的源(含 profile 目录的 .npmrc);读不到返回空串。 */
|
|
11
|
+
export declare function userNpmRegistry(cwd: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* 传输层失败才值得换源:超时、连接重置、DNS、5xx。
|
|
14
|
+
* 404 / 版本不存在 / 鉴权失败必须保持原样 —— 换源只会掩盖作者侧问题或装到滞后版本。
|
|
15
|
+
*/
|
|
16
|
+
export declare function isTransportFailure(text: string): boolean;
|
|
17
|
+
/** 日志里展示用的源主机名(不打印可能带凭据的完整 URL)。 */
|
|
18
|
+
export declare function registryHost(value: string): string;
|
|
@@ -38,6 +38,15 @@ export declare function cancelTask(id: number): boolean;
|
|
|
38
38
|
* 导出供单测覆盖路径推导。
|
|
39
39
|
*/
|
|
40
40
|
export declare function desktopCliBootstrap(entry: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* 传给 CLI 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
|
|
43
|
+
* 其次更新走 `@latest`,git 源与全局 npm 安装保持原样。
|
|
44
|
+
*/
|
|
45
|
+
export declare function npmInstallTargetOf(target: string, opts: {
|
|
46
|
+
update: boolean;
|
|
47
|
+
global: boolean;
|
|
48
|
+
pinned?: string;
|
|
49
|
+
}): string;
|
|
41
50
|
/**
|
|
42
51
|
* 插件变更入队并返回其任务。任务严格串行:队列 worker 只在上一个结束后才启动下一个。
|
|
43
52
|
* 完成前可通过 `getTask(id)` / `activeTask()` 查看进度。
|
|
@@ -62,6 +71,11 @@ export declare function startPluginMutation(options: {
|
|
|
62
71
|
/** 自定义安装入口渠道(NPM 包卡片):任务输出行与系统日志据此溯源「从哪个入口发起」;
|
|
63
72
|
* 目录插件安装不传 */
|
|
64
73
|
installChannel?: 'npm';
|
|
74
|
+
/** npm 源候选(首选在前):传输层失败时按顺序换源重试,空/单个表示不降级 */
|
|
75
|
+
registries?: string[];
|
|
76
|
+
/** 精确安装 spec(如 `dsh-plugin@2.0.5`):Hub 自我更新提示的是已过供应链门禁的版本,
|
|
77
|
+
* 必须钉住安装,不能再用 @latest 被带回门禁内的最新版 */
|
|
78
|
+
installSpec?: string;
|
|
65
79
|
}): InstallTask;
|
|
66
80
|
/**
|
|
67
81
|
* 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
|
|
@@ -101,4 +115,8 @@ export declare function runPluginMutation(options: {
|
|
|
101
115
|
loader?: LoaderHandle;
|
|
102
116
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
103
117
|
globalNpm?: string[];
|
|
118
|
+
/** npm 源候选(首选在前):传输层失败时按顺序换源重试 */
|
|
119
|
+
registries?: string[];
|
|
120
|
+
/** 精确安装 spec(如 `dsh-plugin@2.0.5`):见 startPluginMutation 同名说明 */
|
|
121
|
+
installSpec?: string;
|
|
104
122
|
}): Promise<InstallResult>;
|
|
@@ -3,8 +3,10 @@ export interface HubSettings {
|
|
|
3
3
|
checkUpdatesOnStart: boolean;
|
|
4
4
|
/** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
|
|
5
5
|
proxy: string;
|
|
6
|
-
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm
|
|
6
|
+
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置) */
|
|
7
7
|
npmRegistry: string;
|
|
8
|
+
/** 公共源传输失败时自动换下一个镜像重试(自定义/内网源不切换) */
|
|
9
|
+
registryFallback: boolean;
|
|
8
10
|
/** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
|
|
9
11
|
enableNpmInstall: boolean;
|
|
10
12
|
/** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log;可填目录或 .log 文件路径) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 10000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):10000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -76,6 +76,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
76
76
|
const [confirmPlugin, setConfirmPlugin] = useState<HubPlugin | null>(null)
|
|
77
77
|
/** 弹窗动作是否为「更新」:已安装插件点「更新」→ 走同一条 add 命令原位覆盖重装,文案区分安装/更新 */
|
|
78
78
|
const [confirmIsUpdate, setConfirmIsUpdate] = useState(false)
|
|
79
|
+
/** Hub 自我更新指定的精确版本:连续发版时提示的是「已过供应链门禁」的旧一版,
|
|
80
|
+
* 安装必须钉在该版本上,否则又会被 @latest 带回门禁内的最新版。 */
|
|
81
|
+
const [confirmPinVersion, setConfirmPinVersion] = useState<string | null>(null)
|
|
79
82
|
/** 命令行安装确认弹窗:记录待安装的裸目标(npm 包名)——
|
|
80
83
|
* 与应用商店同一套确认/进度/结果弹窗,提供时 plugin 传 null 走 customTarget 模式 */
|
|
81
84
|
const [confirmCustomTarget, setConfirmCustomTarget] = useState<string | null>(null)
|
|
@@ -450,6 +453,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
450
453
|
onInstall: (p, opts) => {
|
|
451
454
|
setInstallDone(false)
|
|
452
455
|
setConfirmIsUpdate(opts?.update ?? false)
|
|
456
|
+
setConfirmPinVersion(null)
|
|
453
457
|
// 与卸载一致:重置弹窗关联任务,避免新弹窗误匹配到进行中的任务而禁用安装按钮
|
|
454
458
|
queue.clearModalTask()
|
|
455
459
|
setConfirmPlugin(p)
|
|
@@ -484,6 +488,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
484
488
|
setDetailItem(null)
|
|
485
489
|
setInstallDone(false)
|
|
486
490
|
setConfirmIsUpdate(true)
|
|
491
|
+
setConfirmPinVersion(null)
|
|
487
492
|
queue.clearModalTask()
|
|
488
493
|
setConfirmPlugin(item.plugin)
|
|
489
494
|
},
|
|
@@ -547,7 +552,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
547
552
|
// 暂缓提示显示同一标题但只给「稍后再说」,无更新显示当前版本 + 记录 + 「已是最新」。
|
|
548
553
|
// 版本号与本机版本一律由 hubUpdateState / hubCurrentVersion 决定,不用远端版号冒充本机。
|
|
549
554
|
showHubUpdate && h(HubUpdateModal, {
|
|
550
|
-
|
|
555
|
+
// 提示目标是「已过供应链门禁」的版本,可能不是 latest —— 展示它与它的变更记录
|
|
556
|
+
info: catalog.hubUpdateTarget ?? catalog.hubUpdateInfo ?? { version: PLUGIN_VERSION },
|
|
551
557
|
state: catalog.hubUpdateState,
|
|
552
558
|
currentVersion: catalog.hubCurrentVersion,
|
|
553
559
|
lang,
|
|
@@ -559,6 +565,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
559
565
|
if (hubEntry) {
|
|
560
566
|
setInstallDone(false)
|
|
561
567
|
setConfirmIsUpdate(true)
|
|
568
|
+
// 钉住本次提示的版本:最新版可能仍在门禁内,@latest 会被 pnpm 拒绝
|
|
569
|
+
setConfirmPinVersion(catalog.hubUpdateTarget?.version ?? null)
|
|
562
570
|
queue.clearModalTask()
|
|
563
571
|
setConfirmPlugin(hubEntry)
|
|
564
572
|
}
|
|
@@ -595,6 +603,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
595
603
|
setShowNotifications(false)
|
|
596
604
|
setInstallDone(false)
|
|
597
605
|
setConfirmIsUpdate(true)
|
|
606
|
+
setConfirmPinVersion(null)
|
|
598
607
|
queue.clearModalTask()
|
|
599
608
|
setConfirmPlugin(plugin)
|
|
600
609
|
},
|
|
@@ -673,7 +682,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
673
682
|
if (reload) window.location.reload()
|
|
674
683
|
},
|
|
675
684
|
onCopy: () => copyCommand(confirmPlugin),
|
|
676
|
-
onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true } : undefined),
|
|
685
|
+
onInstall: () => queue.installNow(confirmPlugin, confirmIsUpdate ? { update: true, pinVersion: confirmPinVersion ?? undefined } : undefined),
|
|
677
686
|
// 重启:结果视图「立即重启」也会中断进行中的任务 → 先弹重启确认弹窗
|
|
678
687
|
onRestart: () => setShowRestartConfirm(true),
|
|
679
688
|
}),
|
|
@@ -14,7 +14,7 @@ import type { MouseEvent } from 'react'
|
|
|
14
14
|
import styles from '../../styles/Modal.module.css'
|
|
15
15
|
import type { EnvInfo, Translate } from '../../types.ts'
|
|
16
16
|
import type { NotificationRecord } from '../../logic/failures.ts'
|
|
17
|
-
import { actionOfRecord, classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf } from '../../logic/failures.ts'
|
|
17
|
+
import { actionOfRecord, classifyFailure, formatFailureMessage, missingRegistryPackageOf, npmTooLowVersion, pnpmPolicyHintOf, pnpmPolicyHintParams, registryMissingHintOf, unreachableTargetOf, registryHostOf, authRegistryHostOf, exclusiveProfileOf, desktopCliFallbackOf, dshTooOldInfoOf, missingLocalDirOf } from '../../logic/failures.ts'
|
|
18
18
|
import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
|
|
19
19
|
import { pluginIssueUrl, pluginSiteUrl, hubIssueUrl } from '../../logic/urls.ts'
|
|
20
20
|
import { CloseIcon } from '../ui/icons.tsx'
|
|
@@ -306,6 +306,11 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
306
306
|
// 给 .npmrc 豁免指引,不引导提 Issue
|
|
307
307
|
return h('div', { className: styles.failPrepareHint }, t('failPnpmWorkspaceHint'))
|
|
308
308
|
}
|
|
309
|
+
if (kind === 'pnpmVirtualStore') {
|
|
310
|
+
// profile 的 modules 目录与当前 pnpm 的 virtual-store-dir-max-length 不一致:
|
|
311
|
+
// 该 profile 内的安装/卸载都会失败 —— 重建 modules 目录或统一配置即可,不引导提 Issue
|
|
312
|
+
return h('div', { className: styles.failPrepareHint }, t('failPnpmVirtualStoreHint'))
|
|
313
|
+
}
|
|
309
314
|
if (kind === 'pnpmStore') {
|
|
310
315
|
// pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录的依赖是
|
|
311
316
|
// 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
|
|
@@ -372,10 +377,13 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
372
377
|
return h('div', { className: styles.failPrepareHint }, t('failRegistryVersionStaleHint'))
|
|
373
378
|
}
|
|
374
379
|
if (kind === 'pnpmLinkedDirMissing') {
|
|
375
|
-
// profile 里留着指向不存在目录的本地依赖(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
return h('div',
|
|
380
|
+
// profile 里留着指向不存在目录的本地依赖(ERR_PNPM_LINKED_PKG_DIR_NOT_FOUND /
|
|
381
|
+
// pnpm ENOENT lstat):点名缺失目录 + 删除悬空条目,不引导提 Issue
|
|
382
|
+
const dir = missingLocalDirOf(r.message)
|
|
383
|
+
return h('div', null, [
|
|
384
|
+
dir ? h('div', { className: styles.failNetworkTarget }, t('failLinkedDirTarget', { path: dir })) : null,
|
|
385
|
+
h('div', { className: styles.failPrepareHint }, t('failPnpmLinkedDirMissingHint')),
|
|
386
|
+
])
|
|
379
387
|
}
|
|
380
388
|
if (kind === 'fileLocked') {
|
|
381
389
|
// profile 里的文件被其他进程占用(Windows os error 32):通常是宿主或杀毒软件
|
|
@@ -397,6 +405,55 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
397
405
|
// 安装流程,与插件无关 —— 引导从本机地址重新打开市场,不引导提 Issue
|
|
398
406
|
return h('div', { className: styles.failPrepareHint }, t('failOriginRejectedHint', { profile: env?.profile ?? '<profile>' }))
|
|
399
407
|
}
|
|
408
|
+
if (kind === 'profileExclusive') {
|
|
409
|
+
// desktop profile 只能由桌面应用管理:外部 dsh CLI 被宿主拒绝,属环境约束。
|
|
410
|
+
// 带 [desktop-cli-fallback] 则是安装器没找到应用内 CLI 引导脚本 —— 我们的问题,给反馈入口
|
|
411
|
+
const profile = exclusiveProfileOf(r.message)
|
|
412
|
+
const ourBug = desktopCliFallbackOf(r.message)
|
|
413
|
+
return h('div', null, [
|
|
414
|
+
h('div', { className: styles.failPrepareHint },
|
|
415
|
+
t(profile ? 'failProfileExclusiveHint' : 'failProfileExclusiveHintNoProfile', profile ? { profile } : undefined)),
|
|
416
|
+
ourBug ? h('div', { className: styles.failPrepareHint }, t('failDesktopCliFallbackHint')) : null,
|
|
417
|
+
ourBug ? h('a', {
|
|
418
|
+
className: styles.failBigIssue,
|
|
419
|
+
href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts, 'triage', action),
|
|
420
|
+
target: '_blank',
|
|
421
|
+
rel: 'noopener noreferrer',
|
|
422
|
+
title: t('failHubIssueHint'),
|
|
423
|
+
}, t('failIssueBig')) : null,
|
|
424
|
+
])
|
|
425
|
+
}
|
|
426
|
+
if (kind === 'registryAuth') {
|
|
427
|
+
// npm 源返回 401/403:私有源需要凭据,或当前网络/IP 不被允许 —— 本机环境问题,
|
|
428
|
+
// 给「配认证 / 换回官方源 / 切公共镜像」指引,不引导提 Issue
|
|
429
|
+
const host = authRegistryHostOf(r.message)
|
|
430
|
+
return h('div', { className: styles.failPrepareHint },
|
|
431
|
+
t(host ? 'failRegistryAuthHint' : 'failRegistryAuthHintNoHost', host ? { host } : undefined))
|
|
432
|
+
}
|
|
433
|
+
if (kind === 'dshTooOld') {
|
|
434
|
+
// 插件要求的 DSH 下限高于本机宿主:升级 DSH 即可,不属作者、也不属插件中心
|
|
435
|
+
const info = dshTooOldInfoOf(r.message)
|
|
436
|
+
return h('div', { className: styles.failPrepareHint },
|
|
437
|
+
t('failDshTooOldHint', info ?? { pkg: shownRepo || r.repo || 'plugin', floor: '?', current: '?' }))
|
|
438
|
+
}
|
|
439
|
+
if (kind === 'localEnv') {
|
|
440
|
+
// 未命中具体成因、但特征指向本机环境:给通用自查清单 + 系统诊断,默认不推给我们,
|
|
441
|
+
// 只留小字反馈入口(判据是我们自己的启发式,可能误判)
|
|
442
|
+
return h('div', null, [
|
|
443
|
+
h('div', { className: styles.failPrepareHint }, t('failLocalEnvHint')),
|
|
444
|
+
onRunDiagnostics ? h('button', { className: styles.failDiagBtn, onClick: onRunDiagnostics }, t('failNetworkRunDiag')) : null,
|
|
445
|
+
h('div', null, [
|
|
446
|
+
h('div', { className: styles.failPrepareHint }, t('failMisjudgeHint')),
|
|
447
|
+
h('a', {
|
|
448
|
+
className: styles.failMisjudgeLink,
|
|
449
|
+
href: hubIssueUrl(shownRepo || r.repo, r.message, env, r.command, r.attempts, 'triage', action),
|
|
450
|
+
target: '_blank',
|
|
451
|
+
rel: 'noopener noreferrer',
|
|
452
|
+
title: t('failHubIssueHint'),
|
|
453
|
+
}, t('failMisjudgeLink')),
|
|
454
|
+
]),
|
|
455
|
+
])
|
|
456
|
+
}
|
|
400
457
|
if (kind === 'network') {
|
|
401
458
|
// 安装前网络探测 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
402
459
|
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|