dsh-plugin 1.3.8 → 1.3.10
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 +12 -12
- package/README.md +12 -12
- package/client/client.js +385 -315
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +29 -1
- package/lib/services/install/task-queue.js +55 -9
- package/lib/types/services/install/install-types.d.ts +4 -0
- package/lib/types/services/install/task-queue.d.ts +5 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +16 -3
- package/src/client/components/modals/NotificationsModal.tsx +16 -0
- package/src/client/components/modals/modals.tsx +14 -1
- package/src/client/components/views/CustomInstallView.tsx +9 -7
- package/src/client/hooks/useTaskQueue.ts +47 -11
- package/src/client/locales.ts +18 -0
- package/src/client/logic/failures.ts +33 -3
- package/src/client/logic/urls.ts +10 -0
- package/src/client/types.ts +10 -0
- package/src/server/http/routes.ts +28 -1
- package/src/server/services/install/install-types.ts +4 -1
- package/src/server/services/install/task-queue.ts +58 -9
package/lib/http/routes.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { homedir, release } from 'node:os';
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
|
-
import { spawn } from 'node:child_process';
|
|
14
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
15
15
|
import { fetchViaCurl, gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
|
|
16
16
|
import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, globalNpmPackagesOf, hasQueuedTarget, installTargetOf, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName } from '../services/install/install.js';
|
|
17
17
|
import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/profile/installed-versions.js';
|
|
@@ -145,11 +145,29 @@ function hostDshVersion() {
|
|
|
145
145
|
}
|
|
146
146
|
return null;
|
|
147
147
|
}
|
|
148
|
+
/** 探测命令行工具的版本号(pnpm --version / npm --version / git --version):
|
|
149
|
+
* 未安装 / 不在 PATH / 超时一律返回 null —— issue 环境快照如实写 unknown,绝不挂起。
|
|
150
|
+
* (同步探测仅用于启动期/请求期的低频率 /env 快照,安装通道不经过这里。) */
|
|
151
|
+
function toolVersion(cmd, args) {
|
|
152
|
+
try {
|
|
153
|
+
const r = spawnSync(cmd, args, { timeout: 1500, encoding: 'utf8' });
|
|
154
|
+
if (r.error || r.status !== 0)
|
|
155
|
+
return null;
|
|
156
|
+
const v = (r.stdout ?? '').trim().split(/\r?\n/)[0];
|
|
157
|
+
return v || null;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
148
163
|
/** 宿主环境快照:提交 bug 时随 issue 附上,便于作者复现(前端 /env 端点返回)。 */
|
|
149
164
|
function hostEnv(profile) {
|
|
150
165
|
return {
|
|
151
166
|
dshVersion: hostDshVersion(),
|
|
152
167
|
nodeVersion: process.version,
|
|
168
|
+
pnpmVersion: toolVersion('pnpm', ['--version']),
|
|
169
|
+
npmVersion: toolVersion('npm', ['--version']),
|
|
170
|
+
gitVersion: toolVersion('git', ['--version']),
|
|
153
171
|
platform: process.platform,
|
|
154
172
|
arch: process.arch,
|
|
155
173
|
release: release(),
|
|
@@ -650,6 +668,12 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
650
668
|
const source = typeof body === 'object' && body !== null && typeof body.source === 'string'
|
|
651
669
|
? body.source
|
|
652
670
|
: '';
|
|
671
|
+
// 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
|
|
672
|
+
// 客户端显式上报,服务端日志/报错据此精准溯源「从哪个入口发起」;目录插件安装不传。
|
|
673
|
+
const installChannel = (() => {
|
|
674
|
+
const raw = body !== null && typeof body === 'object' ? body.installChannel : undefined;
|
|
675
|
+
return raw === 'git' || raw === 'dsh' || raw === 'npm' ? raw : undefined;
|
|
676
|
+
})();
|
|
653
677
|
const settings = loadSettings(profile);
|
|
654
678
|
// 界面语言(客户端随请求携带):错误消息按用户当前语言提示(中文界面给中文,英文界面给英文)
|
|
655
679
|
const lang = typeof body === 'object' && body !== null && body.lang === 'en'
|
|
@@ -688,6 +712,8 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
688
712
|
globalNpm,
|
|
689
713
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
690
714
|
env: mutationEnv(settings),
|
|
715
|
+
// 全局 npm 安装由 NPM 包卡片/官方命令发起:日志体现入口渠道
|
|
716
|
+
installChannel: installChannel ?? 'npm',
|
|
691
717
|
});
|
|
692
718
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
693
719
|
return;
|
|
@@ -821,6 +847,8 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
821
847
|
env: mutationEnv(settings),
|
|
822
848
|
// 覆盖更新:npm 通道命令显式 @latest(pnpm add 无版本对已存在依赖是幂等的,不加版本不会真更新)
|
|
823
849
|
updateNpm: mode === 'update',
|
|
850
|
+
// 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):日志/报错据此溯源
|
|
851
|
+
installChannel,
|
|
824
852
|
});
|
|
825
853
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
826
854
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 每个任务一次 `dsh plugin --profile <profile> <action> <target>` 子进程,
|
|
8
8
|
* 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
|
|
9
9
|
*/
|
|
10
|
-
import { spawn } from 'node:child_process';
|
|
10
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
11
11
|
import { readFileSync, statSync } from 'node:fs';
|
|
12
12
|
import { dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.js';
|
|
@@ -40,12 +40,12 @@ export function activeTask() {
|
|
|
40
40
|
for (const item of queue) {
|
|
41
41
|
const task = item.task;
|
|
42
42
|
if (task.status === 'pending') {
|
|
43
|
-
found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts });
|
|
43
|
+
found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel });
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
tasks.forEach((task) => {
|
|
47
47
|
if (task.status === 'running') {
|
|
48
|
-
found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts });
|
|
48
|
+
found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel });
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
return found;
|
|
@@ -124,6 +124,23 @@ function cliInvocation() {
|
|
|
124
124
|
}
|
|
125
125
|
return { file: 'dsh', prefixArgs: [], cwd: process.cwd(), useShell: process.platform === 'win32' };
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
|
|
129
|
+
* Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
|
|
130
|
+
* 无法匹配),POSIX 非 shell 模式才会抛 spawn dsh ENOENT —— 两种形态都不可靠,统一在
|
|
131
|
+
* spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
|
|
132
|
+
* 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
|
|
133
|
+
*/
|
|
134
|
+
function dshOnPath(env) {
|
|
135
|
+
try {
|
|
136
|
+
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
137
|
+
const res = spawnSync(probe, ['dsh'], { env, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
138
|
+
return res.status === 0 && (res.stdout ?? '').trim().length > 0;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
127
144
|
function stopChild(child) {
|
|
128
145
|
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
129
146
|
const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
|
|
@@ -150,13 +167,24 @@ function mutationCategory(action) {
|
|
|
150
167
|
function mutationVerb(action) {
|
|
151
168
|
return action === 'remove' ? '开始卸载' : action === 'update' ? '开始更新' : '开始安装';
|
|
152
169
|
}
|
|
170
|
+
/** 系统日志入口渠道后缀:自定义安装(带渠道)在日志里体现「从哪张卡片发起」,
|
|
171
|
+
* 目录插件安装(无渠道)返回空串。日志中文语境,渠道名用中文口语。 */
|
|
172
|
+
function channelLabel(channel) {
|
|
173
|
+
if (channel === 'git')
|
|
174
|
+
return '(GitHub 源码入口)';
|
|
175
|
+
if (channel === 'dsh')
|
|
176
|
+
return '(DSH 命令入口)';
|
|
177
|
+
if (channel === 'npm')
|
|
178
|
+
return '(NPM 包入口)';
|
|
179
|
+
return '';
|
|
180
|
+
}
|
|
153
181
|
/**
|
|
154
182
|
* Enqueue a plugin mutation and return its task. Tasks run strictly serially:
|
|
155
183
|
* the queue worker starts the next one only after the previous finishes.
|
|
156
184
|
* Progress is visible through `getTask(id)` / `activeTask()` until done.
|
|
157
185
|
*/
|
|
158
186
|
export function startPluginMutation(options) {
|
|
159
|
-
const task = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false };
|
|
187
|
+
const task = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false, installChannel: options.installChannel };
|
|
160
188
|
nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1;
|
|
161
189
|
tasks.set(task.id, task);
|
|
162
190
|
if (tasks.size > MAX_TASKS) {
|
|
@@ -169,14 +197,18 @@ export function startPluginMutation(options) {
|
|
|
169
197
|
});
|
|
170
198
|
}
|
|
171
199
|
queue.push({ task, options: { ...options } });
|
|
172
|
-
//
|
|
200
|
+
// 任务输出首行标记入口渠道(ASCII 标记,乱码免疫):进度面板/失败明细里能溯源「从哪张卡片发起」
|
|
201
|
+
if (options.installChannel !== undefined) {
|
|
202
|
+
pushLine(task, `[install-channel] ${options.installChannel}`);
|
|
203
|
+
}
|
|
204
|
+
// 系统日志:记录任务入队(安装/卸载/更新开始),自定义安装带入口渠道后缀
|
|
173
205
|
const category = mutationCategory(options.action);
|
|
174
206
|
appendLog(options.profile, {
|
|
175
207
|
at: Date.now(),
|
|
176
208
|
level: 'info',
|
|
177
209
|
category,
|
|
178
210
|
event: `${category}.start`,
|
|
179
|
-
message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}`,
|
|
211
|
+
message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}${channelLabel(options.installChannel)}`,
|
|
180
212
|
});
|
|
181
213
|
pumpQueue();
|
|
182
214
|
return task;
|
|
@@ -202,7 +234,7 @@ function pumpQueue() {
|
|
|
202
234
|
level: 'success',
|
|
203
235
|
category,
|
|
204
236
|
event: `${category}.done`,
|
|
205
|
-
message: `${verb('成功')} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
|
|
237
|
+
message: `${verb('成功')} ${show}${channelLabel(options.installChannel)}${task.needsRestart ? '(待重启生效)' : ''}`,
|
|
206
238
|
});
|
|
207
239
|
}
|
|
208
240
|
else if (task.status === 'failed') {
|
|
@@ -211,7 +243,7 @@ function pumpQueue() {
|
|
|
211
243
|
level: 'error',
|
|
212
244
|
category,
|
|
213
245
|
event: `${category}.fail`,
|
|
214
|
-
message: `${verb('失败')} ${show}`,
|
|
246
|
+
message: `${verb('失败')} ${show}${channelLabel(options.installChannel)}`,
|
|
215
247
|
});
|
|
216
248
|
}
|
|
217
249
|
else if (task.status === 'cancelled') {
|
|
@@ -220,7 +252,7 @@ function pumpQueue() {
|
|
|
220
252
|
level: 'warn',
|
|
221
253
|
category,
|
|
222
254
|
event: 'task.cancel',
|
|
223
|
-
message: `已取消 ${show}`,
|
|
255
|
+
message: `已取消 ${show}${channelLabel(options.installChannel)}`,
|
|
224
256
|
});
|
|
225
257
|
}
|
|
226
258
|
// 已终态的任务移出队列:既防 queue 数组无限增长(内存泄漏),也让下一轮 pump 干净
|
|
@@ -273,6 +305,20 @@ function spawnMutation(options) {
|
|
|
273
305
|
let settled = false;
|
|
274
306
|
let child;
|
|
275
307
|
try {
|
|
308
|
+
// 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
|
|
309
|
+
// 命令不存在时 Windows shell 模式只回传 GBK 乱码、POSIX 才抛 spawn dsh ENOENT —— 统一前置
|
|
310
|
+
// 拦截并打 ASCII 标记 [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
|
|
311
|
+
if (invocation.file === 'dsh' && !dshOnPath(env)) {
|
|
312
|
+
const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)';
|
|
313
|
+
if (task) {
|
|
314
|
+
task.status = 'failed';
|
|
315
|
+
task.exitCode = null;
|
|
316
|
+
task.progress = 0;
|
|
317
|
+
pushLine(task, detail);
|
|
318
|
+
}
|
|
319
|
+
resolveOnce({ exitCode: 1, timedOut: false, error: detail, stdout: '', stderr: detail });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
276
322
|
child = spawn(invocation.file, attemptArgs, {
|
|
277
323
|
cwd: invocation.cwd,
|
|
278
324
|
env,
|
|
@@ -26,6 +26,9 @@ export interface InstallTask {
|
|
|
26
26
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
|
|
27
27
|
globalNpm?: string[];
|
|
28
28
|
action: 'add' | 'remove' | 'update';
|
|
29
|
+
/** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
|
|
30
|
+
* 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装无此字段。 */
|
|
31
|
+
installChannel?: 'npm' | 'git' | 'dsh';
|
|
29
32
|
/** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
|
|
30
33
|
status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
|
|
31
34
|
timedOut: boolean;
|
|
@@ -54,6 +57,7 @@ export interface QueueItem {
|
|
|
54
57
|
timeoutMs?: number;
|
|
55
58
|
env?: NodeJS.ProcessEnv;
|
|
56
59
|
globalNpm?: string[];
|
|
60
|
+
installChannel?: 'npm' | 'git' | 'dsh';
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
/**
|
|
@@ -11,6 +11,8 @@ export interface ActiveTaskInfo {
|
|
|
11
11
|
lines: string[];
|
|
12
12
|
/** 尝试过的安装方式(npm 反查 + 实际执行命令):失败时前端 issue 预填需要 */
|
|
13
13
|
attempts: string[];
|
|
14
|
+
/** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):前端刷新恢复后仍能精准溯源 */
|
|
15
|
+
installChannel?: 'npm' | 'git' | 'dsh';
|
|
14
16
|
}
|
|
15
17
|
/** All non-terminal tasks in queue order (running first, then pending). Lets the client resume a queue after a page reload. */
|
|
16
18
|
export declare function activeTask(): ActiveTaskInfo[];
|
|
@@ -52,6 +54,9 @@ export declare function startPluginMutation(options: {
|
|
|
52
54
|
attempts?: string[];
|
|
53
55
|
/** 更新已安装的 npm 包(mode=update):命令显式带 @latest,让 pnpm 真正解析最新版本 */
|
|
54
56
|
updateNpm?: boolean;
|
|
57
|
+
/** 自定义安装入口渠道(客户端三张卡片:NPM 包 / GitHub 源码 / DSH 命令):
|
|
58
|
+
* 任务输出行与系统日志据此溯源「从哪个入口发起」;目录插件安装不传 */
|
|
59
|
+
installChannel?: 'npm' | 'git' | 'dsh';
|
|
55
60
|
}): InstallTask;
|
|
56
61
|
/**
|
|
57
62
|
* 安装后校验已装包的入口文件(package.json 的 main / exports["."].default)是否真实存在。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 4000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):4000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -12,7 +12,7 @@ import { createElement as h, useEffect, useRef, useState } from 'react'
|
|
|
12
12
|
import type { MouseEvent } from 'react'
|
|
13
13
|
import styles from '../styles/Header.module.css'
|
|
14
14
|
import modalStyles from '../styles/Modal.module.css'
|
|
15
|
-
import type { EnvInfo, HubPlugin, SectionProps, ToastState } from '../types.ts'
|
|
15
|
+
import type { EnvInfo, HubPlugin, InstallChannel, SectionProps, ToastState } from '../types.ts'
|
|
16
16
|
import { installCommandOf, repoFromInstallTarget } from '../logic/install-command.ts'
|
|
17
17
|
import { PLUGIN_VERSION } from '../logic/constants.ts'
|
|
18
18
|
import { ensurePluginCss } from '../logic/ensure-plugin-css.ts'
|
|
@@ -81,6 +81,9 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
81
81
|
/** 命令行安装确认弹窗:记录待安装的裸目标(npm 包名 / GitHub 地址,DSH 命令已剥成裸目标)——
|
|
82
82
|
* 与应用商店同一套确认/进度/结果弹窗,提供时 plugin 传 null 走 customTarget 模式 */
|
|
83
83
|
const [confirmCustomTarget, setConfirmCustomTarget] = useState<string | null>(null)
|
|
84
|
+
/** 自定义安装入口渠道(三张卡片之一):随确认弹窗记录,提交时上报服务端,
|
|
85
|
+
* 报错/日志据此精准溯源「从哪个入口发起」(npm 全局安装固定为 'npm') */
|
|
86
|
+
const [confirmCustomChannel, setConfirmCustomChannel] = useState<InstallChannel | null>(null)
|
|
84
87
|
/** 全局 npm 安装确认弹窗(官方 `npm install -g <pkgs>`):记录原始命令 + 解析出的包列表,
|
|
85
88
|
* 走与应用商店同一套确认/进度/结果弹窗(plugin 传 null,InstallModal 走 globalNpm 模式) */
|
|
86
89
|
const [confirmGlobalNpm, setConfirmGlobalNpm] = useState<{ raw: string; pkgs: string[] } | null>(null)
|
|
@@ -212,6 +215,7 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
212
215
|
if (e.key === 'Escape') {
|
|
213
216
|
setConfirmPlugin(null)
|
|
214
217
|
setConfirmCustomTarget(null)
|
|
218
|
+
setConfirmCustomChannel(null)
|
|
215
219
|
setConfirmGlobalNpm(null)
|
|
216
220
|
setUninstallPlugin(null)
|
|
217
221
|
setUninstallItem(null)
|
|
@@ -471,11 +475,14 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
471
475
|
// 不进插件列表,不做「已安装→更新」判定 —— npm install -g 对已装包是原位覆盖更新,天然幂等)
|
|
472
476
|
if (opts?.globalNpm && opts.globalNpm.length > 0) {
|
|
473
477
|
setConfirmCustomTarget(null)
|
|
478
|
+
setConfirmCustomChannel(null)
|
|
474
479
|
setConfirmIsUpdate(false)
|
|
475
480
|
setConfirmGlobalNpm({ raw, pkgs: opts.globalNpm })
|
|
476
481
|
return
|
|
477
482
|
}
|
|
478
483
|
setConfirmGlobalNpm(null)
|
|
484
|
+
// 记录入口渠道:报错/日志据此精准提示「从哪张卡片发起的安装」(NPM 包 / GitHub 源码 / DSH 命令)
|
|
485
|
+
setConfirmCustomChannel(opts.installChannel)
|
|
479
486
|
setConfirmIsUpdate(customTargetInstalled(target))
|
|
480
487
|
setConfirmCustomTarget(target)
|
|
481
488
|
},
|
|
@@ -635,12 +642,17 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
635
642
|
onClose: () => {
|
|
636
643
|
setInstallDone(false)
|
|
637
644
|
setConfirmCustomTarget(null)
|
|
645
|
+
setConfirmCustomChannel(null)
|
|
638
646
|
},
|
|
639
647
|
onCopy: () => {
|
|
640
648
|
doCopy(`pnpm add ${confirmCustomTarget}`)
|
|
641
649
|
setToast({ id: Date.now(), kind: 'copied' })
|
|
642
650
|
},
|
|
643
|
-
onInstall: () => void queue.installCustom(confirmCustomTarget,
|
|
651
|
+
onInstall: () => void queue.installCustom(confirmCustomTarget, {
|
|
652
|
+
update: confirmIsUpdate,
|
|
653
|
+
// 入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):上报服务端,报错/日志据此溯源
|
|
654
|
+
channel: confirmCustomChannel ?? 'npm',
|
|
655
|
+
}),
|
|
644
656
|
onRestart: () => setShowRestartConfirm(true),
|
|
645
657
|
}),
|
|
646
658
|
// 全局 npm 安装确认弹窗(官方 `npm install -g <pkgs>`):与应用商店同一套确认/进度/结果流程。
|
|
@@ -661,12 +673,13 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
661
673
|
onClose: () => {
|
|
662
674
|
setInstallDone(false)
|
|
663
675
|
setConfirmGlobalNpm(null)
|
|
676
|
+
setConfirmCustomChannel(null)
|
|
664
677
|
},
|
|
665
678
|
onCopy: () => {
|
|
666
679
|
doCopy(`npm install -g ${confirmGlobalNpm.pkgs.join(' ')}`)
|
|
667
680
|
setToast({ id: Date.now(), kind: 'copied' })
|
|
668
681
|
},
|
|
669
|
-
onInstall: () => void queue.installGlobalNpm(confirmGlobalNpm.pkgs),
|
|
682
|
+
onInstall: () => void queue.installGlobalNpm(confirmGlobalNpm.pkgs, { channel: 'npm' }),
|
|
670
683
|
onRestart: () => setShowRestartConfirm(true),
|
|
671
684
|
}),
|
|
672
685
|
// 卸载确认弹窗:确认/进度/结果视图逻辑收敛在 modals.tsx。
|
|
@@ -266,6 +266,22 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
266
266
|
return h('div', { className: styles.failPrepareHint },
|
|
267
267
|
t(v ? 'failNpmTooLowV' : 'failNpmTooLow', v ? { v } : undefined))
|
|
268
268
|
}
|
|
269
|
+
if (kind === 'dshMissing') {
|
|
270
|
+
// 找不到 dsh 命令(Win「不是内部或外部命令」/ POSIX「command not found」/ spawn ENOENT):
|
|
271
|
+
// 本机 DeepSeek Harness 未正确安装或 dsh 不在 PATH,不是插件问题 —— 直接给环境修复指引,不引导提 Issue
|
|
272
|
+
return h('div', { className: styles.failPrepareHint }, t('failDshMissingHint'))
|
|
273
|
+
}
|
|
274
|
+
if (kind === 'pnpmMissing') {
|
|
275
|
+
// 找到了 dsh 但缺 pnpm(dsh 用它管理 profile 插件;dsh 报 pnpm not found on PATH):
|
|
276
|
+
// 本机缺 pnpm,不是插件问题 —— 给安装指引,不引导提 Issue
|
|
277
|
+
return h('div', { className: styles.failPrepareHint }, t('failPnpmMissingHint'))
|
|
278
|
+
}
|
|
279
|
+
if (kind === 'pnpmStore') {
|
|
280
|
+
// pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录旧依赖是
|
|
281
|
+
// 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
|
|
282
|
+
// 给清理重建指引,不引导提 Issue
|
|
283
|
+
return h('div', { className: styles.failPrepareHint }, t('failPnpmStoreHint'))
|
|
284
|
+
}
|
|
269
285
|
if (kind === 'network') {
|
|
270
286
|
// 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
271
287
|
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
@@ -387,7 +387,20 @@ export function ErrorModal({ message, repo, kind, command, attempts, t, env, onC
|
|
|
387
387
|
failureKind === 'npmTooOld'
|
|
388
388
|
? h('div', { className: styles.failPrepareHint },
|
|
389
389
|
t(npmVersion ? 'failNpmTooLowV' : 'failNpmTooLow', npmVersion ? { v: npmVersion } : undefined))
|
|
390
|
-
: failureKind === '
|
|
390
|
+
: failureKind === 'dshMissing'
|
|
391
|
+
// 找不到 dsh 命令(Win「不是内部或外部命令」/ POSIX「command not found」/ spawn ENOENT):
|
|
392
|
+
// 本机 DeepSeek Harness 未正确安装或 dsh 不在 PATH,不是插件问题 —— 直接给环境修复指引,不引导提 Issue
|
|
393
|
+
? h('div', { className: styles.failPrepareHint }, t('failDshMissingHint'))
|
|
394
|
+
: failureKind === 'pnpmMissing'
|
|
395
|
+
// 找到了 dsh 但缺 pnpm(dsh 用它管理 profile 插件;dsh 报 pnpm not found on PATH):
|
|
396
|
+
// 本机缺 pnpm,不是插件问题 —— 给安装指引,不引导提 Issue
|
|
397
|
+
? h('div', { className: styles.failPrepareHint }, t('failPnpmMissingHint'))
|
|
398
|
+
: failureKind === 'pnpmStore'
|
|
399
|
+
// pnpm 存在但 store 大版本不匹配(ERR_PNPM_UNEXPECTED_STORE):profile 目录旧依赖是
|
|
400
|
+
// 另一大版本 pnpm 装的,当前 pnpm 不认,任何插件装进该 profile 都会失败 ——
|
|
401
|
+
// 给清理重建指引,不引导提 Issue
|
|
402
|
+
? h('div', { className: styles.failPrepareHint }, t('failPnpmStoreHint'))
|
|
403
|
+
: failureKind === 'network'
|
|
391
404
|
// 安装前预检 / 安装日志里的连接失败([network] / git fetch 超时 / DNS / TLS):
|
|
392
405
|
// 是本机网络不通或代理有问题,不是插件问题 —— 提示检查网络 + 直达「系统诊断」,
|
|
393
406
|
// 不引导提 Issue
|
|
@@ -20,7 +20,7 @@ import { createElement as h, useState } from 'react'
|
|
|
20
20
|
import type { FormEvent, MouseEvent } from 'react'
|
|
21
21
|
import styles from '../../styles/CustomInstallView.module.css'
|
|
22
22
|
import modal from '../../styles/Modal.module.css'
|
|
23
|
-
import type { Translate } from '../../types.ts'
|
|
23
|
+
import type { InstallChannel, Translate } from '../../types.ts'
|
|
24
24
|
import { CloseIcon, HelpIcon } from '../ui/icons.tsx'
|
|
25
25
|
|
|
26
26
|
/* —— 自定义安装输入预检 ——
|
|
@@ -113,8 +113,10 @@ type HelpTarget = keyof typeof HELP_CARDS
|
|
|
113
113
|
export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, enableDsh, onOpenSettings, profile }: {
|
|
114
114
|
t: Translate
|
|
115
115
|
/** 命令行安装:输入 npm 包名 / npm、pnpm 安装命令 / GitHub 地址 / dsh plugin 命令即装
|
|
116
|
-
* (custom
|
|
117
|
-
|
|
116
|
+
* (custom 源,受安全设置开关控制)。installChannel 必填 = 入口渠道(三张卡片之一):
|
|
117
|
+
* 服务端日志/报错提示据此溯源「从哪个入口发起」;opts.globalNpm 表示官方 npm 全局安装
|
|
118
|
+
* 命令(npm install -g),附带解析出的包列表(仍属 NPM 包入口)。 */
|
|
119
|
+
onInstallCustom: (raw: string, opts: { installChannel: InstallChannel; globalNpm?: string[] }) => void
|
|
118
120
|
/** 安全信任三通道开关:关掉后对应输入卡片禁用(不提交),并引导去设置打开 */
|
|
119
121
|
enableNpm: boolean
|
|
120
122
|
enableGit: boolean
|
|
@@ -155,9 +157,9 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
|
|
|
155
157
|
const parsed = parseNpmInput(raw)
|
|
156
158
|
if (parsed === null) { setNpmError(t('npmInstallInvalid')); return }
|
|
157
159
|
if (parsed.kind === 'global') {
|
|
158
|
-
onInstallCustom(raw, { globalNpm: parsed.pkgs })
|
|
160
|
+
onInstallCustom(raw, { installChannel: 'npm', globalNpm: parsed.pkgs })
|
|
159
161
|
} else {
|
|
160
|
-
onInstallCustom(parsed.target)
|
|
162
|
+
onInstallCustom(parsed.target, { installChannel: 'npm' })
|
|
161
163
|
}
|
|
162
164
|
setNpmQuery('')
|
|
163
165
|
setNpmError('')
|
|
@@ -166,7 +168,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
|
|
|
166
168
|
const target = gitQuery.trim()
|
|
167
169
|
if (!target) return
|
|
168
170
|
if (!isGitHubInput(target)) { setGitError(t('gitInstallInvalid')); return }
|
|
169
|
-
onInstallCustom(target)
|
|
171
|
+
onInstallCustom(target, { installChannel: 'git' })
|
|
170
172
|
setGitQuery('')
|
|
171
173
|
setGitError('')
|
|
172
174
|
}
|
|
@@ -177,7 +179,7 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
|
|
|
177
179
|
if (parsed === null) { setCmdError(t('dshCmdInvalid')); return }
|
|
178
180
|
// add / update 统一走安装流程:目标已安装时弹窗自动转「更新」覆盖重装(服务端 mode=update 放行),
|
|
179
181
|
// 未安装时即为普通安装 —— 粘贴 `dsh plugin --profile web update dsh-plugin` 这类命令即可更新到最新
|
|
180
|
-
onInstallCustom(parsed.target)
|
|
182
|
+
onInstallCustom(parsed.target, { installChannel: 'dsh' })
|
|
181
183
|
setCmdQuery('')
|
|
182
184
|
setCmdError('')
|
|
183
185
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* uninstall, cancel) plus the modal task lookups used by the dialogs.
|
|
11
11
|
*/
|
|
12
12
|
import { useEffect, useRef, useState } from 'react'
|
|
13
|
-
import type { HubPlugin, LocaleId, Translate } from '../types.ts'
|
|
13
|
+
import type { HubPlugin, InstallChannel, LocaleId, Translate } from '../types.ts'
|
|
14
14
|
import { installCommandOf, installTargetOf, repoFromInstallTarget } from '../logic/install-command.ts'
|
|
15
15
|
import type { InstalledItem } from '../logic/installed.ts'
|
|
16
16
|
|
|
@@ -28,6 +28,9 @@ export interface QueueTask {
|
|
|
28
28
|
desc?: string
|
|
29
29
|
/** 所属插件仓库(owner/repo):失败时错误弹窗据此提供「去仓库反馈」入口;卸载为 null */
|
|
30
30
|
repo: string | null
|
|
31
|
+
/** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令三张卡片):失败时错误/通知精准
|
|
32
|
+
* 溯源「从哪个入口发起」;目录插件安装无此字段(非自定义入口)。 */
|
|
33
|
+
channel?: InstallChannel
|
|
31
34
|
/** 实际动作(add/update/remove):成功通知据此区分「安装成功」与「更新成功」。
|
|
32
35
|
* 服务端 /active 带回真实动作;本地乐观入队按 update 标记推断。 */
|
|
33
36
|
action?: 'add' | 'update' | 'remove'
|
|
@@ -89,6 +92,15 @@ export interface TaskQueueOptions {
|
|
|
89
92
|
resolvePending: (repo: string) => { desc?: string; version?: string } | null
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/** 入口渠道的 i18n key:NPM 包 / GitHub 源码 / DSH 命令。 */
|
|
96
|
+
const channelKeyOf = (channel: InstallChannel): string =>
|
|
97
|
+
channel === 'git' ? 'installChannelGit' : channel === 'dsh' ? 'installChannelDsh' : 'installChannelNpm'
|
|
98
|
+
|
|
99
|
+
/** 失败消息的入口溯源前缀:自定义安装(带渠道)在错误正文顶部精准指出从哪张卡片发起;
|
|
100
|
+
* 目录插件安装(无渠道)返回 null,错误正文保持原样。 */
|
|
101
|
+
const entryLineOf = (t: Translate, channel: InstallChannel | undefined): string | null =>
|
|
102
|
+
channel ? t('installEntryLine', { c: t(channelKeyOf(channel)) }) : null
|
|
103
|
+
|
|
92
104
|
export function useTaskQueue(opts: TaskQueueOptions) {
|
|
93
105
|
const {
|
|
94
106
|
t, langKey, refreshInstalled, onInstallDone, onUninstallDone, onError,
|
|
@@ -205,10 +217,12 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
205
217
|
}
|
|
206
218
|
} else {
|
|
207
219
|
// 失败:完整展示全部输出行(最新在前,逆序为日志阅读顺序),不裁剪
|
|
208
|
-
const
|
|
220
|
+
const rawDetail = lines.length > 0
|
|
209
221
|
? [...lines].reverse().join('\n')
|
|
210
222
|
: q.kind === 'uninstall' ? t('uninstallFail') : t('installFail')
|
|
211
|
-
|
|
223
|
+
// 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」,目录安装保持原样
|
|
224
|
+
const entry = entryLineOf(t, q.channel)
|
|
225
|
+
onError(entry ? `${entry}\n${rawDetail}` : rawDetail, q.repo, q.kind, q.command, q.attempts, q.action === 'update')
|
|
212
226
|
}
|
|
213
227
|
maybeStopPoll()
|
|
214
228
|
}
|
|
@@ -283,7 +297,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
283
297
|
const res = await fetch('/dsh-plugin-hub/active', { cache: 'no-store' })
|
|
284
298
|
if (!res.ok) throw new Error(`active ${res.status}`)
|
|
285
299
|
const data = await res.json() as {
|
|
286
|
-
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown }[]
|
|
300
|
+
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown; installChannel?: unknown }[]
|
|
287
301
|
pendingRestarts?: unknown
|
|
288
302
|
}
|
|
289
303
|
const active = (data.tasks ?? []).filter((a) => typeof a.id === 'number')
|
|
@@ -313,6 +327,10 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
313
327
|
lines: Array.isArray(a.lines) ? (a.lines as string[]) : (prevTask?.lines ?? []),
|
|
314
328
|
attempts: Array.isArray(a.attempts) ? (a.attempts as string[]) : (prevTask?.attempts ?? []),
|
|
315
329
|
needsRestart: typeof a.needsRestart === 'boolean' ? a.needsRestart : (prevTask?.needsRestart ?? true),
|
|
330
|
+
// 入口渠道:本地乐观入队已带;服务端 /active 也带回(刷新恢复用),服务端优先
|
|
331
|
+
channel: (a.installChannel === 'npm' || a.installChannel === 'git' || a.installChannel === 'dsh')
|
|
332
|
+
? a.installChannel
|
|
333
|
+
: prevTask?.channel,
|
|
316
334
|
})
|
|
317
335
|
}
|
|
318
336
|
// 保留乐观条目:服务端尚未登记(preflight 中),无真实 id,不能因 active 缺它就被合并吞掉
|
|
@@ -353,7 +371,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
353
371
|
const res = await fetch('/dsh-plugin-hub/active', { cache: 'no-store' })
|
|
354
372
|
if (!res.ok) return
|
|
355
373
|
const data = await res.json() as {
|
|
356
|
-
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown }[]
|
|
374
|
+
tasks?: { id?: unknown; action?: unknown; target?: unknown; status?: unknown; progress?: unknown; lines?: unknown; attempts?: unknown; needsRestart?: unknown; displayTarget?: unknown; installChannel?: unknown }[]
|
|
357
375
|
pendingRestarts?: unknown
|
|
358
376
|
}
|
|
359
377
|
if (cancelled) return
|
|
@@ -379,6 +397,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
379
397
|
lines: Array.isArray(a.lines) ? (a.lines as string[]) : [],
|
|
380
398
|
attempts: Array.isArray((a as { attempts?: unknown }).attempts) ? ((a as { attempts: string[] }).attempts) : [],
|
|
381
399
|
needsRestart: typeof (a as { needsRestart?: unknown }).needsRestart === 'boolean' ? (a as { needsRestart: boolean }).needsRestart : true,
|
|
400
|
+
// 服务端登记的入口渠道:自定义安装刷新后也能精准溯源(目录安装无此字段)
|
|
401
|
+
channel: (a.installChannel === 'npm' || a.installChannel === 'git' || a.installChannel === 'dsh') ? a.installChannel : undefined,
|
|
382
402
|
}
|
|
383
403
|
})
|
|
384
404
|
if (items.length > 0 || pendingRef.current.length > 0) {
|
|
@@ -410,6 +430,9 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
410
430
|
update?: boolean
|
|
411
431
|
/** 全局 npm 安装包列表(`npm install -g <pkgs>`):非空时服务端执行全局安装,不进任何 profile */
|
|
412
432
|
globalNpm?: string[]
|
|
433
|
+
/** 自定义安装入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):随请求上报服务端落日志,
|
|
434
|
+
* 失败弹窗/通知据此精准溯源「从哪个入口发起」;目录安装不传。 */
|
|
435
|
+
channel?: InstallChannel
|
|
413
436
|
}) => {
|
|
414
437
|
// 防重复入队:同一目标已在排队/执行中则忽略
|
|
415
438
|
if (queueRef.current.some((q) => q.kind === 'install' && q.target === input.repo)) return
|
|
@@ -428,6 +451,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
428
451
|
version: input.version, updatedAt: input.updatedAt,
|
|
429
452
|
status: 'running', progress: 0, lines: [], optimistic: true,
|
|
430
453
|
command: input.command,
|
|
454
|
+
channel: input.channel,
|
|
431
455
|
action: input.update ? 'update' : 'add',
|
|
432
456
|
// 服务端登记后 /active 会带回真实尝试记录(npm 反查 + 执行命令),先占位空列表
|
|
433
457
|
attempts: [], needsRestart: true,
|
|
@@ -446,6 +470,8 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
446
470
|
mode: input.update ? 'update' : undefined,
|
|
447
471
|
// 界面语言带给服务端:网络预检等错误消息按用户当前语言返回(中文界面给中文,英文界面给英文)
|
|
448
472
|
...(langKey ? { lang: langKey } : {}),
|
|
473
|
+
// 自定义安装入口渠道(三张卡片之一):服务端日志/报错提示据此溯源
|
|
474
|
+
...(input.channel ? { installChannel: input.channel } : {}),
|
|
449
475
|
...(input.globalNpm && input.globalNpm.length > 0 ? { globalNpm: input.globalNpm } : {}),
|
|
450
476
|
}),
|
|
451
477
|
signal: controller.signal,
|
|
@@ -455,8 +481,11 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
455
481
|
// 请求层失败(重复安装 409 / 参数错误 / 安全开关 403 等):移除乐观条目 + 完整错误弹窗
|
|
456
482
|
applyQueue((prev) => prev.filter((x) => x.id !== tempId))
|
|
457
483
|
// 同步 400(如预检拦截)服务端会附上已尝试的安装方式,issue 预填一并展示;
|
|
458
|
-
// 用 || 而非 ?? :error 为空字符串时回退 HTTP
|
|
459
|
-
|
|
484
|
+
// 用 || 而非 ?? :error 为空字符串时回退 HTTP 状态,避免复制出来是空白。
|
|
485
|
+
// 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」
|
|
486
|
+
const entry = entryLineOf(t, input.channel)
|
|
487
|
+
const msg = data.error || `HTTP ${res.status}`
|
|
488
|
+
onError(entry ? `${entry}\n${msg}` : msg, input.repo, 'install', input.command, data.attempts)
|
|
460
489
|
return
|
|
461
490
|
}
|
|
462
491
|
const taskId = data.task
|
|
@@ -476,7 +505,10 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
476
505
|
const reason = aborted
|
|
477
506
|
? t('requestTimeout')
|
|
478
507
|
: err instanceof Error && err.message ? err.message : String(err ?? '')
|
|
479
|
-
|
|
508
|
+
// 自定义安装(带入口渠道):错误正文顶部精准提示「从哪张卡片发起」
|
|
509
|
+
const entry = entryLineOf(t, input.channel)
|
|
510
|
+
const msg = reason ? `${t('installFail')} — ${reason}` : t('installFail')
|
|
511
|
+
onError(entry ? `${entry}\n${msg}` : msg, input.repo, 'install', input.command)
|
|
480
512
|
} finally {
|
|
481
513
|
window.clearTimeout(timeout)
|
|
482
514
|
submittingRef.current.delete(input.repo)
|
|
@@ -505,9 +537,11 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
505
537
|
|
|
506
538
|
/** 命令行安装:用户手输 npm 包名或 GitHub 地址,走 custom 源(受安全开关控制)。
|
|
507
539
|
* update=true 时对已安装目标放行覆盖重装(与目录插件「更新」同一语义)。
|
|
540
|
+
* channel=入口渠道(NPM 包 / GitHub 源码 / DSH 命令卡片):上报服务端落日志,
|
|
541
|
+
* 失败弹窗/通知据此精准溯源。
|
|
508
542
|
* 队列展示/防重一律用归一化身份(owner/repo 或 npm 包名):与服务端 /active 合并、
|
|
509
543
|
* 弹窗目标匹配同口径,npm 包名透传不受影响。 */
|
|
510
|
-
const installCustom = async (raw: string, opts?: { update?: boolean }) => {
|
|
544
|
+
const installCustom = async (raw: string, opts?: { update?: boolean; channel?: InstallChannel }) => {
|
|
511
545
|
const target = raw.trim()
|
|
512
546
|
if (!target) return
|
|
513
547
|
const repo = repoFromInstallTarget(target)
|
|
@@ -517,12 +551,13 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
517
551
|
command: `pnpm add ${target}`,
|
|
518
552
|
source: 'custom',
|
|
519
553
|
update: opts?.update,
|
|
554
|
+
channel: opts?.channel,
|
|
520
555
|
})
|
|
521
556
|
}
|
|
522
557
|
|
|
523
558
|
/** 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`):不进任何 profile、无需宿主重启。
|
|
524
|
-
* 队列展示/防重用包列表空格拼接的身份(与服务端 target
|
|
525
|
-
const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean }) => {
|
|
559
|
+
* 队列展示/防重用包列表空格拼接的身份(与服务端 target 同口径);入口渠道固定为 npm(NPM 包卡片)。 */
|
|
560
|
+
const installGlobalNpm = async (pkgs: string[], opts?: { update?: boolean; channel?: InstallChannel }) => {
|
|
526
561
|
const list = (pkgs ?? []).map((p) => p.trim()).filter((p) => p !== '')
|
|
527
562
|
if (list.length === 0) return
|
|
528
563
|
const identity = list.join(' ')
|
|
@@ -533,6 +568,7 @@ export function useTaskQueue(opts: TaskQueueOptions) {
|
|
|
533
568
|
source: 'custom',
|
|
534
569
|
globalNpm: list,
|
|
535
570
|
update: opts?.update,
|
|
571
|
+
channel: opts?.channel ?? 'npm',
|
|
536
572
|
})
|
|
537
573
|
}
|
|
538
574
|
|