dsh-plugin 1.3.4 → 1.3.6
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 +38 -1
- package/README.md +38 -1
- package/client/client.js +449 -410
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +9 -5
- package/lib/services/install/npm-resolve.js +3 -1
- package/lib/services/install/task-queue.js +29 -15
- package/lib/services/profile/profile.js +4 -3
- package/lib/services/settings.js +1 -1
- package/lib/types/services/install/install-types.d.ts +2 -2
- package/lib/types/services/install/npm-resolve.d.ts +3 -1
- package/lib/types/services/install/task-queue.d.ts +3 -3
- package/lib/types/services/settings.d.ts +2 -2
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +13 -7
- package/src/client/components/modals/NotificationsModal.tsx +9 -3
- package/src/client/components/views/CustomInstallView.tsx +35 -14
- package/src/client/components/views/DiagnosticsView.tsx +7 -3
- package/src/client/components/views/SettingsView.tsx +20 -33
- package/src/client/hooks/useSettings.ts +3 -3
- package/src/client/hooks/useTaskQueue.ts +12 -6
- package/src/client/locales.ts +31 -15
- package/src/client/logic/failures.ts +3 -0
- package/src/client/logic/renderMarkdown.ts +9 -5
- package/src/client/logic/urls.ts +12 -1
- package/src/client/styles/CustomInstallView.module.css +25 -0
- package/src/client/styles/Dropdown.module.css +10 -0
- package/src/client/styles/Modal.module.css +11 -0
- package/src/server/http/routes.ts +9 -4
- package/src/server/services/install/install-types.ts +2 -2
- package/src/server/services/install/npm-resolve.ts +3 -1
- package/src/server/services/install/task-queue.ts +36 -20
- package/src/server/services/profile/profile.ts +4 -3
- package/src/server/services/settings.ts +3 -3
package/lib/http/routes.js
CHANGED
|
@@ -157,13 +157,12 @@ function hostEnv(profile) {
|
|
|
157
157
|
dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
-
/** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入
|
|
160
|
+
/** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 HTTP(S) 代理。
|
|
161
|
+
* npm registry 不再注入 —— 安装完全沿用用户本机 npm 配置(~/.npmrc / 全局配置)。
|
|
161
162
|
* 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
|
|
162
163
|
* 使安装通道与诊断使用相同的代理来源。 */
|
|
163
164
|
function mutationEnv(settings) {
|
|
164
165
|
const env = { ...process.env, CI: 'true' };
|
|
165
|
-
if (settings.npmRegistry !== '')
|
|
166
|
-
env.npm_config_registry = settings.npmRegistry;
|
|
167
166
|
const proxy = settings.proxy !== '' ? settings.proxy : (systemProxy() ?? '');
|
|
168
167
|
if (proxy !== '') {
|
|
169
168
|
env.HTTP_PROXY = proxy;
|
|
@@ -485,11 +484,14 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
485
484
|
// 空 body / 非 JSON:视为全量探测
|
|
486
485
|
}
|
|
487
486
|
const settings = loadSettings(profile);
|
|
487
|
+
// npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
|
|
488
|
+
// 未配置(空串)时探测官方源作为「跟随本机 npm 配置」的基础连通性参考。
|
|
488
489
|
const registry = settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org';
|
|
489
490
|
// 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
|
|
490
491
|
// 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
|
|
492
|
+
// npm 行 display:配置了镜像就显示镜像地址;未配置置空 —— 客户端显示「未配置(跟随本机 npm 配置)」。
|
|
491
493
|
const allChecks = [
|
|
492
|
-
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ?
|
|
494
|
+
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
493
495
|
{ key: 'github', url: 'https://github.com/dshplugin/dsh-plugin-hub', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/dsh-plugin-hub' },
|
|
494
496
|
{ key: 'catalog', url: 'https://api.dsh-plugin.org/stats.json', display: 'api.dsh-plugin.org', cmd: 'curl -s https://api.dsh-plugin.org/stats.json' },
|
|
495
497
|
];
|
|
@@ -727,7 +729,9 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
727
729
|
}
|
|
728
730
|
// Kick off the CLI in the background; the client polls /status for progress
|
|
729
731
|
const task = startPluginMutation({
|
|
730
|
-
|
|
732
|
+
// mode=update(已安装目标的覆盖更新)以 update 动作入队:
|
|
733
|
+
// 日志按 update 类别落盘(update.start/done/fail),实际 CLI 执行仍是 add <target>@latest
|
|
734
|
+
action: mode === 'update' ? 'update' : 'add',
|
|
731
735
|
profile,
|
|
732
736
|
target,
|
|
733
737
|
// npm 安装时 target 是包名:displayTarget 固定展示仓库名,待重启行/前端恢复不受通道变化影响
|
|
@@ -20,7 +20,9 @@ const REQUEST_TIMEOUT_MS = 8000;
|
|
|
20
20
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
21
21
|
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
22
22
|
* 原有错误路径,反查只是额外的一次尝试。
|
|
23
|
-
*
|
|
23
|
+
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
24
|
+
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
25
|
+
* 慢网络下失败不阻塞安装。
|
|
24
26
|
*/
|
|
25
27
|
export function resolveNpmPackage(repo, registry = '') {
|
|
26
28
|
const key = repo.toLowerCase();
|
|
@@ -142,6 +142,14 @@ function stopChild(child) {
|
|
|
142
142
|
}
|
|
143
143
|
child.kill('SIGKILL');
|
|
144
144
|
}
|
|
145
|
+
/** 系统日志类别:安装/卸载/更新三分,针对性地判断「哪一步不兼容」。 */
|
|
146
|
+
function mutationCategory(action) {
|
|
147
|
+
return action === 'remove' ? 'uninstall' : action === 'update' ? 'update' : 'install';
|
|
148
|
+
}
|
|
149
|
+
/** 系统日志动作词:开始安装/开始卸载/开始更新。 */
|
|
150
|
+
function mutationVerb(action) {
|
|
151
|
+
return action === 'remove' ? '开始卸载' : action === 'update' ? '开始更新' : '开始安装';
|
|
152
|
+
}
|
|
145
153
|
/**
|
|
146
154
|
* Enqueue a plugin mutation and return its task. Tasks run strictly serially:
|
|
147
155
|
* the queue worker starts the next one only after the previous finishes.
|
|
@@ -161,14 +169,14 @@ export function startPluginMutation(options) {
|
|
|
161
169
|
});
|
|
162
170
|
}
|
|
163
171
|
queue.push({ task, options: { ...options } });
|
|
164
|
-
//
|
|
165
|
-
const category = options.action
|
|
172
|
+
// 系统日志:记录任务入队(安装/卸载/更新开始)
|
|
173
|
+
const category = mutationCategory(options.action);
|
|
166
174
|
appendLog(options.profile, {
|
|
167
175
|
at: Date.now(),
|
|
168
176
|
level: 'info',
|
|
169
177
|
category,
|
|
170
178
|
event: `${category}.start`,
|
|
171
|
-
message: `${options.action
|
|
179
|
+
message: `${mutationVerb(options.action)} ${options.displayTarget ?? options.target}`,
|
|
172
180
|
});
|
|
173
181
|
pumpQueue();
|
|
174
182
|
return task;
|
|
@@ -184,16 +192,17 @@ function pumpQueue() {
|
|
|
184
192
|
task.status = 'running';
|
|
185
193
|
void runPluginMutation({ ...options, task }).finally(() => {
|
|
186
194
|
runningChildren.delete(task.id);
|
|
187
|
-
//
|
|
195
|
+
// 系统日志:任务终态(成功/失败/取消),按动作归类安装/卸载/更新日志
|
|
188
196
|
const show = task.displayTarget ?? task.target;
|
|
189
|
-
const category = task.action
|
|
197
|
+
const category = mutationCategory(task.action);
|
|
198
|
+
const verb = (v) => task.action === 'remove' ? `卸载${v}` : task.action === 'update' ? `更新${v}` : `安装${v}`;
|
|
190
199
|
if (task.status === 'done') {
|
|
191
200
|
appendLog(options.profile, {
|
|
192
201
|
at: Date.now(),
|
|
193
202
|
level: 'success',
|
|
194
203
|
category,
|
|
195
204
|
event: `${category}.done`,
|
|
196
|
-
message: `${
|
|
205
|
+
message: `${verb('成功')} ${show}${task.needsRestart ? '(待重启生效)' : ''}`,
|
|
197
206
|
});
|
|
198
207
|
}
|
|
199
208
|
else if (task.status === 'failed') {
|
|
@@ -202,7 +211,7 @@ function pumpQueue() {
|
|
|
202
211
|
level: 'error',
|
|
203
212
|
category,
|
|
204
213
|
event: `${category}.fail`,
|
|
205
|
-
message: `${
|
|
214
|
+
message: `${verb('失败')} ${show}`,
|
|
206
215
|
});
|
|
207
216
|
}
|
|
208
217
|
else if (task.status === 'cancelled') {
|
|
@@ -236,17 +245,21 @@ function spawnMutation(options) {
|
|
|
236
245
|
// 更新已安装的 npm 包:显式指定 @latest。pnpm add <pkg>(无版本)对已存在依赖
|
|
237
246
|
// (尤其精确 spec 如 "1.3.0")是幂等的 —— 输出 "Already up to date" 且不改 package.json,
|
|
238
247
|
// 导致「更新成功」但实际仍停留在旧版本;git 地址与全局安装不受此影响。
|
|
239
|
-
|
|
248
|
+
// update 动作实际执行 `add <target>@latest`(pnpm 对已存在依赖带显式版本即原位覆盖升级),
|
|
249
|
+
// CLI 动词仍透传 add —— 不执行官方 update 动词,避免与既有覆盖重装机制分叉。
|
|
250
|
+
const isUpdate = action === 'update' || updateNpm === true;
|
|
251
|
+
const cliAction = action === 'update' ? 'add' : action;
|
|
252
|
+
const npmUpdateTarget = isUpdate && !isGlobalNpm && githubRepoOf(target) === null
|
|
240
253
|
? `${target}@latest`
|
|
241
254
|
: target;
|
|
242
255
|
const args = isGlobalNpm
|
|
243
256
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
244
|
-
: [...invocation.prefixArgs, 'plugin', '--profile', profile,
|
|
257
|
+
: [...invocation.prefixArgs, 'plugin', '--profile', profile, cliAction, npmUpdateTarget];
|
|
245
258
|
// 记录实际执行的命令(npm 通道显示包名,git 通道显示 HTTPS 源),
|
|
246
259
|
// 失败提 Issue 时作为「已尝试的安装方式」随附
|
|
247
260
|
const executed = isGlobalNpm
|
|
248
261
|
? `npm install -g ${(globalNpm ?? []).join(' ')}`
|
|
249
|
-
: `dsh plugin --profile ${profile} ${
|
|
262
|
+
: `dsh plugin --profile ${profile} ${cliAction} ${npmUpdateTarget}`;
|
|
250
263
|
if (task && !task.attempts.includes(executed)) {
|
|
251
264
|
task.attempts.push(executed);
|
|
252
265
|
}
|
|
@@ -413,10 +426,10 @@ function spawnMutation(options) {
|
|
|
413
426
|
addPendingRestart(displayTarget ?? target, 'uninstall');
|
|
414
427
|
}
|
|
415
428
|
else {
|
|
416
|
-
//
|
|
429
|
+
// 安装/更新成功(或卸载但无 loader 引用)→ 登记待重启:
|
|
417
430
|
// 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
|
|
418
431
|
task.needsRestart = true;
|
|
419
|
-
addPendingRestart(displayTarget ?? target, action === '
|
|
432
|
+
addPendingRestart(displayTarget ?? target, action === 'remove' ? 'uninstall' : 'install');
|
|
420
433
|
}
|
|
421
434
|
}
|
|
422
435
|
pushLine(task, result.timedOut ? '[timed out]' : `[exit ${result.exitCode ?? '?'}]`);
|
|
@@ -482,7 +495,7 @@ function ignoredBuildKeys(output) {
|
|
|
482
495
|
* 前端据此展示「本机 npm 版本过低」的准确原因 —— 避免该报错被外层 ERR_PNPM_PREPARE_PACKAGE
|
|
483
496
|
* 误判成插件打包/分发问题、误导用户提 Issue。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
|
|
484
497
|
function annotateNpmTooLow(result, options) {
|
|
485
|
-
if (options.action
|
|
498
|
+
if (options.action === 'remove' || result.exitCode === 0 || result.timedOut)
|
|
486
499
|
return result;
|
|
487
500
|
const line = npmTooLowMarker(`${result.stderr}\n${result.stdout}`, options.env);
|
|
488
501
|
if (line === null)
|
|
@@ -504,7 +517,8 @@ export async function runPluginMutation(options) {
|
|
|
504
517
|
let result = await spawnMutation(options);
|
|
505
518
|
// allowBuilds 放行 / 装后入口校验都是 profile 专属(pnpm 装进 profile 的场景);
|
|
506
519
|
// 全局 npm 安装直接走 npm,无 pnpm allowBuilds 拦截、也不进 profile,跳过这两类恢复
|
|
507
|
-
|
|
520
|
+
// 安装与更新(同为 add 通道执行)共享同一套恢复路径;卸载不适用
|
|
521
|
+
if (options.action !== 'remove' && result.exitCode !== 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
|
|
508
522
|
const output = `${result.stderr}\n${result.stdout}`;
|
|
509
523
|
// git 源:pnpm 打印的 key@url 提示
|
|
510
524
|
if (output.includes('ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED')) {
|
|
@@ -544,7 +558,7 @@ export async function runPluginMutation(options) {
|
|
|
544
558
|
// pnpm 安装本身成功,但目标包入口文件缺失(git 分发缺构建产物)→ 判为失败:
|
|
545
559
|
// 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
|
|
546
560
|
// 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
|
|
547
|
-
if (options.action
|
|
561
|
+
if (options.action !== 'remove' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
|
|
548
562
|
const verified = verifyInstalledEntry(options.profile, options.target);
|
|
549
563
|
if (verified.name !== null && verified.missing !== null) {
|
|
550
564
|
clearPendingRestart(options.displayTarget ?? options.target);
|
|
@@ -27,16 +27,17 @@ export function githubTarget(repo) {
|
|
|
27
27
|
return `git+https://github.com/${repo}.git`;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> add <target>`)—— 提取其中的安装目标;
|
|
30
|
+
* DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> <add|update> <target>`)—— 提取其中的安装目标;
|
|
31
31
|
* 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 必填且只有长选项
|
|
32
32
|
* (官方 CLI 无 -p 简写,省略即报错),支持 `--profile=<name>` 等号形式,命令大小写不敏感。
|
|
33
|
+
* update = 更新已安装目标到最新;remove 不在此列(卸载走 /uninstall 路由,按已装包名直卸)。
|
|
33
34
|
* 与客户端 CustomInstallView 口径一致。
|
|
34
35
|
*/
|
|
35
|
-
const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+add\s+(.+)$/i;
|
|
36
|
+
const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+(add|update)\s+(.+)$/i;
|
|
36
37
|
export function installTargetOf(value) {
|
|
37
38
|
const input = typeof value === 'string' ? value.trim() : '';
|
|
38
39
|
const match = DSH_PLUGIN_CMD_RE.exec(input);
|
|
39
|
-
return match !== null ? match[
|
|
40
|
+
return match !== null ? match[2].trim() : input;
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
42
43
|
* npm 全局安装命令(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令)——
|
package/lib/services/settings.js
CHANGED
|
@@ -12,8 +12,8 @@ import { dirname, join } from 'node:path';
|
|
|
12
12
|
import { profileDirectory } from './profile/profile.js';
|
|
13
13
|
export const DEFAULT_SETTINGS = {
|
|
14
14
|
checkUpdatesOnStart: true,
|
|
15
|
-
npmRegistry: '',
|
|
16
15
|
proxy: '',
|
|
16
|
+
npmRegistry: '',
|
|
17
17
|
enableNpmInstall: true,
|
|
18
18
|
enableGitInstall: true,
|
|
19
19
|
enableDshInstall: true,
|
|
@@ -25,7 +25,7 @@ export interface InstallTask {
|
|
|
25
25
|
attempts: string[];
|
|
26
26
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时任务执行全局 npm 安装,不进任何 profile */
|
|
27
27
|
globalNpm?: string[];
|
|
28
|
-
action: 'add' | 'remove';
|
|
28
|
+
action: 'add' | 'remove' | 'update';
|
|
29
29
|
/** 队列语义:pending 排队中 / running 执行中 / done / failed / cancelled(用户取消) */
|
|
30
30
|
status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled';
|
|
31
31
|
timedOut: boolean;
|
|
@@ -48,7 +48,7 @@ export interface Invocation {
|
|
|
48
48
|
export interface QueueItem {
|
|
49
49
|
task: InstallTask;
|
|
50
50
|
options: {
|
|
51
|
-
action: 'add' | 'remove';
|
|
51
|
+
action: 'add' | 'remove' | 'update';
|
|
52
52
|
profile: string;
|
|
53
53
|
target: string;
|
|
54
54
|
timeoutMs?: number;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
3
3
|
* 返回 null 不代表仓库一定没有 npm 包,只代表本次未能确认 —— 调用方应保留
|
|
4
4
|
* 原有错误路径,反查只是额外的一次尝试。
|
|
5
|
-
*
|
|
5
|
+
* registry 参数:npm 镜像源地址,空串 = 官方源;与安装通道吃同一 registry,
|
|
6
|
+
* 保证「配置了镜像」时反查和安装走同一个源(镜像节点同步完整时结果一致)。
|
|
7
|
+
* 慢网络下失败不阻塞安装。
|
|
6
8
|
*/
|
|
7
9
|
export declare function resolveNpmPackage(repo: string, registry?: string): Promise<string | null>;
|
|
@@ -5,7 +5,7 @@ export declare function getTask(id: number): InstallTask | undefined;
|
|
|
5
5
|
export interface ActiveTaskInfo {
|
|
6
6
|
id: number;
|
|
7
7
|
target: string;
|
|
8
|
-
action: 'add' | 'remove';
|
|
8
|
+
action: 'add' | 'remove' | 'update';
|
|
9
9
|
status: 'pending' | 'running';
|
|
10
10
|
progress: number;
|
|
11
11
|
lines: string[];
|
|
@@ -37,7 +37,7 @@ export declare function cancelTask(id: number): boolean;
|
|
|
37
37
|
* Progress is visible through `getTask(id)` / `activeTask()` until done.
|
|
38
38
|
*/
|
|
39
39
|
export declare function startPluginMutation(options: {
|
|
40
|
-
action: 'add' | 'remove';
|
|
40
|
+
action: 'add' | 'remove' | 'update';
|
|
41
41
|
profile: string;
|
|
42
42
|
target: string;
|
|
43
43
|
timeoutMs?: number;
|
|
@@ -73,7 +73,7 @@ export declare function verifyInstalledEntry(profile: string, target: string): {
|
|
|
73
73
|
* Other failures fall through to the single-spawn result.
|
|
74
74
|
*/
|
|
75
75
|
export declare function runPluginMutation(options: {
|
|
76
|
-
action: 'add' | 'remove';
|
|
76
|
+
action: 'add' | 'remove' | 'update';
|
|
77
77
|
profile: string;
|
|
78
78
|
target: string;
|
|
79
79
|
timeoutMs?: number;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export interface HubSettings {
|
|
2
2
|
/** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
|
|
3
3
|
checkUpdatesOnStart: boolean;
|
|
4
|
-
/** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
|
|
5
|
-
npmRegistry: string;
|
|
6
4
|
/** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
|
|
7
5
|
proxy: string;
|
|
6
|
+
/** npm 镜像源 registry 地址(空串 = 未配置,跟随用户本机 npm 配置,本插件不注入任何 registry) */
|
|
7
|
+
npmRegistry: string;
|
|
8
8
|
/** 启用命令行安装 NPM 包(默认开;关掉后输入 npm 包名安装被拦截) */
|
|
9
9
|
enableNpmInstall: boolean;
|
|
10
10
|
/** 启用命令行安装 GitHub 源码(默认开;关掉后输入 GitHub 地址安装被拦截) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.6",
|
|
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",
|
|
@@ -107,12 +107,13 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
107
107
|
const queue = useTaskQueue({
|
|
108
108
|
t,
|
|
109
109
|
refreshInstalled: catalog.refreshInstalled,
|
|
110
|
-
onInstallDone: (viaModal, repo, needsRestart) => {
|
|
110
|
+
onInstallDone: (viaModal, repo, needsRestart, update) => {
|
|
111
111
|
setInstallNeedsRestart(needsRestart)
|
|
112
112
|
if (viaModal) setInstallDone(true)
|
|
113
113
|
else setToast({ id: Date.now(), kind: 'done' })
|
|
114
|
-
//
|
|
115
|
-
|
|
114
|
+
// 安装成功也写入通知记录:通知中心里成功与失败都能看到。
|
|
115
|
+
// action 区分「安装成功」与「更新成功」(更新 = 覆盖重装,同一执行通道)
|
|
116
|
+
setNotifications(addSuccess({ kind: 'install', action: update ? 'update' : 'install', repo: repo ?? '' }))
|
|
116
117
|
},
|
|
117
118
|
onUninstallDone: (viaModal, repo, needsRestart) => {
|
|
118
119
|
setUninstallNeedsRestart(needsRestart)
|
|
@@ -120,13 +121,16 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
120
121
|
else setToast({ id: Date.now(), kind: 'removed' })
|
|
121
122
|
setNotifications(addSuccess({ kind: 'uninstall', repo: repo ?? '' }))
|
|
122
123
|
},
|
|
123
|
-
onError: (message, repo, kind, command, attempts) => {
|
|
124
|
+
onError: (message, repo, kind, command, attempts, update) => {
|
|
124
125
|
// 提 Issue 目标先反查仓库身份:npm 包名能在已安装表/目录里反查到 owner/repo 才保留,
|
|
125
|
-
// 反查不到置 null ——
|
|
126
|
+
// 反查不到置 null —— 错误弹窗据此不显示「一键提 Issue」按钮(防提到错误仓库)
|
|
126
127
|
const issueRepo = issueRepoOf(repo ?? '', catalog.installedItems, catalog.plugins)
|
|
127
128
|
setErrorMsg({ message, repo: issueRepo, kind, command, attempts })
|
|
128
|
-
//
|
|
129
|
-
|
|
129
|
+
// 失败自动写入通知记录:任务在后台结束时没人盯着也能留痕。
|
|
130
|
+
// 存原始 repo(可能只是裸包名)而非反查结果 —— 反查只服务于提 Issue,
|
|
131
|
+
// 通知中心的展示反查由 NotificationsModal 做、查不到会回退显示原始值,
|
|
132
|
+
// 若这里就丢掉原始值,通知里会连名字都看不到;action 区分「安装失败/更新失败」
|
|
133
|
+
setNotifications(addFailure({ kind, action: update ? 'update' : 'install', repo: repo ?? '', message, command, attempts }))
|
|
130
134
|
},
|
|
131
135
|
installPlugin: confirmPlugin,
|
|
132
136
|
installCustomTarget: confirmCustomTarget,
|
|
@@ -427,6 +431,8 @@ export function PluginHubSection({ t: _hostT, locale }: SectionProps) {
|
|
|
427
431
|
enableGit: hubSettings.enableGitInstall,
|
|
428
432
|
enableDsh: hubSettings.enableDshInstall,
|
|
429
433
|
onOpenSettings: () => setView('settings'),
|
|
434
|
+
// 当前 profile:一键插入的 Hub 更新命令用它拼 --profile
|
|
435
|
+
profile: env?.profile ?? 'web',
|
|
430
436
|
onInstallCustom: (raw, opts) => {
|
|
431
437
|
const target = raw.trim()
|
|
432
438
|
if (!target) return
|
|
@@ -193,8 +193,12 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
193
193
|
h('span', {
|
|
194
194
|
className: r.ok ? styles.noticeTextOk : styles.noticeTextFail,
|
|
195
195
|
}, r.ok
|
|
196
|
-
? (isUpdate
|
|
197
|
-
|
|
196
|
+
? (isUpdate
|
|
197
|
+
? t('updateNoticeTitle')
|
|
198
|
+
: (r.action === 'update' ? t('updateDone') : (r.kind === 'install' ? t('installDone') : t('uninstallDone'))))
|
|
199
|
+
: (r.action === 'update'
|
|
200
|
+
? t('errorTitleUpdate')
|
|
201
|
+
: (r.kind === 'install' ? t('errorTitleInstall') : t('errorTitleUninstall')))),
|
|
198
202
|
// 更新提醒:新版本号紧随标题展示;repo 为纯文本(整行点击即去更新,不跳官网外链)
|
|
199
203
|
isUpdate && r.version
|
|
200
204
|
? h('span', { className: styles.noticeVersion, title: r.version }, `v${r.version}`)
|
|
@@ -211,7 +215,9 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
211
215
|
rel: 'noopener noreferrer',
|
|
212
216
|
title: shownRepo,
|
|
213
217
|
}, shownRepo)
|
|
214
|
-
|
|
218
|
+
// 反查不到仓库身份(如命令行安装的裸 npm 包名):回退显示记录里的原始值,
|
|
219
|
+
// 保证通知永不只剩一句「安装/更新成功」没有名字 —— 只是不给官网外链(查不到不敢乱跳)
|
|
220
|
+
: h('span', { className: styles.failRepo, title: r.repo }, r.repo))
|
|
215
221
|
: null,
|
|
216
222
|
!r.ok && h('button', {
|
|
217
223
|
className: styles.failCopy,
|
|
@@ -26,7 +26,8 @@ import { CloseIcon, HelpIcon } from '../ui/icons.tsx'
|
|
|
26
26
|
/* —— 自定义安装输入预检 ——
|
|
27
27
|
* 提交前校验「输入是否符合该通道格式」,识别不了的输入不发请求,就地提示推荐格式。
|
|
28
28
|
* 三个通道严格分开:npm 框认裸包名或完整 npm/pnpm 安装命令(含全局 `npm install -g`),
|
|
29
|
-
* git 框只认 GitHub 地址,命令框只认官方 `dsh plugin --profile <name> add <target
|
|
29
|
+
* git 框只认 GitHub 地址,命令框只认官方 `dsh plugin --profile <name> <add|update> <target>`
|
|
30
|
+
* (--profile 必填,无 -p 简写;update = 更新已安装目标到最新)。
|
|
30
31
|
* 这里只做格式校验,真正归一化/安装由服务端完成。 */
|
|
31
32
|
|
|
32
33
|
/** GitHub 地址(带前缀形态):github:、https://github.com/、git+https://github.com/、git@github.com: */
|
|
@@ -36,10 +37,12 @@ const GITHUB_BARE_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/
|
|
|
36
37
|
/** npm 包名(含 @scope/pkg) */
|
|
37
38
|
const NPM_PACKAGE_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/
|
|
38
39
|
|
|
39
|
-
/** DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> add <target>`)——
|
|
40
|
+
/** DSH 命令行前缀(官方唯一形式 `dsh plugin --profile <name> <add|update> <target>`)—— 提取其中的动作与目标。
|
|
40
41
|
* 官方 CLI(apps/cli/src/args.ts,commander):--profile 为必填长选项(无 -p 简写,省略即报错),
|
|
41
|
-
* 支持 `--profile=<name>`
|
|
42
|
-
|
|
42
|
+
* 支持 `--profile=<name>` 等号形式;命令大小写不敏感。update = 已安装目标覆盖更新到最新;
|
|
43
|
+
* remove 不走这里(卸载由「已安装」列表的卸载按钮执行,命令框不认卸载动词)。
|
|
44
|
+
* 与 server 端 installTargetOf 口径一致。 */
|
|
45
|
+
const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+(add|update)\s+(.+)$/i
|
|
43
46
|
|
|
44
47
|
/** npm 全局安装命令(官方 README 格式,如 `npm install -g @deepseek-ai/dsh @deepseek-harness-tui/dsh-tui`)——
|
|
45
48
|
* 提取 -g 之后的包列表;任一包名非法返回 null(交给格式错误提示)。与 server 端 globalNpmPackagesOf 口径一致。 */
|
|
@@ -49,10 +52,12 @@ const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i
|
|
|
49
52
|
* 提取命令后的包列表;包列表在 splitPackages 里逐个校验。 */
|
|
50
53
|
const NPM_INSTALL_CMD_RE = /^(?:npm\s+(?:install|i)|pnpm\s+(?:add|i|install))\s+(.+)$/i
|
|
51
54
|
|
|
52
|
-
|
|
55
|
+
/** 解析 DSH 命令:返回 { action, target };非官方命令返回 null(交给格式错误提示)。 */
|
|
56
|
+
function parseDshCommand(raw: string): { action: 'add' | 'update'; target: string } | null {
|
|
53
57
|
const input = raw.trim()
|
|
54
58
|
const match = DSH_PLUGIN_CMD_RE.exec(input)
|
|
55
|
-
|
|
59
|
+
if (match === null) return null
|
|
60
|
+
return { action: match[1].toLowerCase() as 'add' | 'update', target: match[2].trim() }
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/** 空格分隔的包列表:逐个按 npm 包名语法校验 + 拒绝以 - 开头的 token(npm 会把 --xxx 当参数而非包名),
|
|
@@ -96,10 +101,6 @@ function isGitHubInput(raw: string): boolean {
|
|
|
96
101
|
return GITHUB_URL_RE.test(input) || GITHUB_BARE_RE.test(input)
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
function isDshCommand(raw: string): boolean {
|
|
100
|
-
return DSH_PLUGIN_CMD_RE.test(raw.trim())
|
|
101
|
-
}
|
|
102
|
-
|
|
103
104
|
/** 卡片标题旁的「帮助」弹窗:三块卡片各自的标题与格式清单语言包 key。 */
|
|
104
105
|
const HELP_CARDS = {
|
|
105
106
|
npm: { titleKey: 'npmInstallLabel', contentKey: 'npmInstallHelp' },
|
|
@@ -109,7 +110,7 @@ const HELP_CARDS = {
|
|
|
109
110
|
|
|
110
111
|
type HelpTarget = keyof typeof HELP_CARDS
|
|
111
112
|
|
|
112
|
-
export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, enableDsh, onOpenSettings }: {
|
|
113
|
+
export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, enableDsh, onOpenSettings, profile }: {
|
|
113
114
|
t: Translate
|
|
114
115
|
/** 命令行安装:输入 npm 包名 / npm、pnpm 安装命令 / GitHub 地址 / dsh plugin 命令即装
|
|
115
116
|
* (custom 源,受安全设置开关控制);opts.globalNpm 表示官方 npm 全局安装命令(npm install -g),附带解析出的包列表 */
|
|
@@ -120,6 +121,8 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
|
|
|
120
121
|
enableDsh: boolean
|
|
121
122
|
/** 引导去「设置 → 安全信任」打开被关闭的通道开关 */
|
|
122
123
|
onOpenSettings: () => void
|
|
124
|
+
/** 当前 dsh profile 名(宿主 /env 快照):一键插入的更新命令用它拼 --profile */
|
|
125
|
+
profile: string
|
|
123
126
|
}) {
|
|
124
127
|
/** 自定义安装:NPM 包输入与格式错误('' = 无错误) */
|
|
125
128
|
const [npmQuery, setNpmQuery] = useState('')
|
|
@@ -170,11 +173,19 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
|
|
|
170
173
|
const submitCmd = () => {
|
|
171
174
|
const raw = cmdQuery.trim()
|
|
172
175
|
if (!raw) return
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
const parsed = parseDshCommand(raw)
|
|
177
|
+
if (parsed === null) { setCmdError(t('dshCmdInvalid')); return }
|
|
178
|
+
// add / update 统一走安装流程:目标已安装时弹窗自动转「更新」覆盖重装(服务端 mode=update 放行),
|
|
179
|
+
// 未安装时即为普通安装 —— 粘贴 `dsh plugin --profile web update dsh-plugin` 这类命令即可更新到最新
|
|
180
|
+
onInstallCustom(parsed.target)
|
|
175
181
|
setCmdQuery('')
|
|
176
182
|
setCmdError('')
|
|
177
183
|
}
|
|
184
|
+
/** 一键插入 DSH Plugin Hub 自身的更新命令:按当前 profile 拼官方命令填进输入框,用户确认后提交 */
|
|
185
|
+
const insertHubUpdate = () => {
|
|
186
|
+
setCmdQuery(`dsh plugin --profile ${profile} update dsh-plugin`)
|
|
187
|
+
setCmdError('')
|
|
188
|
+
}
|
|
178
189
|
|
|
179
190
|
return h('div', { className: styles.root },
|
|
180
191
|
// 页面说明:独立一级导航,交代「装完去哪看」
|
|
@@ -265,7 +276,8 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
|
|
|
265
276
|
gitError ? h('div', { className: styles.installError }, gitError) : null,
|
|
266
277
|
],
|
|
267
278
|
),
|
|
268
|
-
// 命令卡片:粘贴官方 dsh plugin
|
|
279
|
+
// 命令卡片:粘贴官方 dsh plugin 命令即装/即更新 —— 只认完整命令
|
|
280
|
+
// (--profile 必填,可含 = 等号,动词 add/update,剥成裸目标走安装流程)
|
|
269
281
|
h('div', { className: enableDsh ? styles.installCard : `${styles.installCard} ${styles.installCardDisabled}` },
|
|
270
282
|
h('div', { className: styles.installCardHead },
|
|
271
283
|
h('span', { className: styles.installLabel }, t('dshCmdLabel')),
|
|
@@ -276,6 +288,15 @@ export function CustomInstallView({ t, onInstallCustom, enableNpm, enableGit, en
|
|
|
276
288
|
'aria-label': t('installHelp'),
|
|
277
289
|
onClick: () => setHelpFor('cmd'),
|
|
278
290
|
}, h(HelpIcon)),
|
|
291
|
+
// 一键插入 Hub 自身的更新命令:按当前 profile 拼 `dsh plugin --profile <p> update dsh-plugin`
|
|
292
|
+
// 填入输入框(命令卡片被安全开关禁用时不显示,插入后也提交不了)
|
|
293
|
+
enableDsh
|
|
294
|
+
? h('button', {
|
|
295
|
+
type: 'button',
|
|
296
|
+
className: styles.installInsertBtn,
|
|
297
|
+
onClick: insertHubUpdate,
|
|
298
|
+
}, t('dshCmdInsertHubUpdate'))
|
|
299
|
+
: null,
|
|
279
300
|
),
|
|
280
301
|
!enableDsh
|
|
281
302
|
? channelOffRow
|
|
@@ -40,7 +40,8 @@ interface RowState {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const INITIAL_ROWS: RowState[] = [
|
|
43
|
-
|
|
43
|
+
// npm 行 display 初始置空:设置里未配置镜像时显示「未配置(跟随本机)」,探测结果回来再用服务端 display 覆盖
|
|
44
|
+
{ key: 'npm', display: '', nameKey: 'diagNpm', status: 'idle', ms: null, statusCode: null },
|
|
44
45
|
{ key: 'github', display: 'github.com', nameKey: 'diagGithub', status: 'idle', ms: null, statusCode: null },
|
|
45
46
|
{ key: 'catalog', display: 'api.dsh-plugin.org', nameKey: 'diagCatalog', status: 'idle', ms: null, statusCode: null },
|
|
46
47
|
]
|
|
@@ -87,12 +88,14 @@ export function DiagnosticsView({ t, env, onCopy }: {
|
|
|
87
88
|
const raw = buf.slice(0, nl).trim()
|
|
88
89
|
buf = buf.slice(nl + 1)
|
|
89
90
|
if (raw !== '') {
|
|
90
|
-
const ev = JSON.parse(raw) as { type?: string; key?: string; ms?: number | null; status?: number | null }
|
|
91
|
+
const ev = JSON.parse(raw) as { type?: string; key?: string; ms?: number | null; status?: number | null; display?: string }
|
|
91
92
|
if (ev.type === 'ok' || ev.type === 'fail') {
|
|
92
93
|
patchRows(ev.key as ProbeKey, {
|
|
93
94
|
status: ev.type === 'ok' ? 'ok' : 'fail',
|
|
94
95
|
ms: typeof ev.ms === 'number' ? ev.ms : null,
|
|
95
96
|
statusCode: typeof ev.status === 'number' ? ev.status : null,
|
|
97
|
+
// 服务端带出实际使用的源地址(配置镜像 → 镜像地址;未配置 → 空串)
|
|
98
|
+
display: typeof ev.display === 'string' ? ev.display : '',
|
|
96
99
|
})
|
|
97
100
|
}
|
|
98
101
|
}
|
|
@@ -159,7 +162,8 @@ export function DiagnosticsView({ t, env, onCopy }: {
|
|
|
159
162
|
title: t('diagRecheck'),
|
|
160
163
|
},
|
|
161
164
|
h('span', { className: styles.name }, t(r.nameKey)),
|
|
162
|
-
|
|
165
|
+
// npm 行未配置镜像:display 为空 → 显示「未配置(跟随本机)」,不误导成官方源
|
|
166
|
+
h('span', { className: styles.display }, r.display !== '' ? r.display : t('diagNpmUnset')),
|
|
163
167
|
h('span', { className: styles.meta }, r.status === 'ok' && r.ms !== null ? `${r.ms} ms` : ''),
|
|
164
168
|
badge,
|
|
165
169
|
)
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
import { createElement as h, useState } from 'react'
|
|
19
19
|
import type { ChangeEvent, MouseEvent, ReactNode } from 'react'
|
|
20
20
|
import styles from '../../styles/SettingsView.module.css'
|
|
21
|
+
import dropdownStyles from '../../styles/Dropdown.module.css'
|
|
21
22
|
import modalStyles from '../../styles/Modal.module.css'
|
|
22
|
-
import { Dropdown } from '../ui/Dropdown.tsx'
|
|
23
23
|
import { Toggle } from '../ui/Toggle.tsx'
|
|
24
|
+
import { Dropdown } from '../ui/Dropdown.tsx'
|
|
24
25
|
import { DiagnosticsView } from './DiagnosticsView.tsx'
|
|
25
26
|
import { LogsView } from './LogsView.tsx'
|
|
26
27
|
import {
|
|
@@ -31,6 +32,16 @@ import type { HubSettings } from '../../hooks/useSettings.ts'
|
|
|
31
32
|
|
|
32
33
|
type SettingsSection = 'updates' | 'security' | 'diagnostics' | 'logs' | 'reset'
|
|
33
34
|
|
|
35
|
+
/** npm 镜像源预设:空串 = 未配置(跟随用户本机 npm 配置,本插件不注入任何 registry)。
|
|
36
|
+
* 只提供常见国内顶级镜像下拉,不做自定义输入 —— 想用其它镜像直接在 ~/.npmrc 配置即可。 */
|
|
37
|
+
const MIRROR_PRESETS: Array<{ value: string; labelKey: string }> = [
|
|
38
|
+
{ value: '', labelKey: 'mirrorNone' },
|
|
39
|
+
{ value: 'https://registry.npmjs.org', labelKey: 'mirrorOfficial' },
|
|
40
|
+
{ value: 'https://registry.npmmirror.com', labelKey: 'mirrorNpmmirror' },
|
|
41
|
+
{ value: 'https://mirrors.cloud.tencent.com/npm/', labelKey: 'mirrorTencent' },
|
|
42
|
+
{ value: 'https://mirrors.tuna.tsinghua.edu.cn/npm/', labelKey: 'mirrorTsinghua' },
|
|
43
|
+
]
|
|
44
|
+
|
|
34
45
|
/** 左侧导航:顺序即展示顺序;labelKey 是分组标题,icon 是行内小图标。 */
|
|
35
46
|
const NAV: Array<{ key: SettingsSection; labelKey: string; icon: () => ReturnType<typeof h> }> = [
|
|
36
47
|
{ key: 'updates', labelKey: 'settingsUpdate', icon: UpdatesIcon },
|
|
@@ -40,13 +51,6 @@ const NAV: Array<{ key: SettingsSection; labelKey: string; icon: () => ReturnTyp
|
|
|
40
51
|
{ key: 'reset', labelKey: 'settingsReset', icon: ResetIcon },
|
|
41
52
|
]
|
|
42
53
|
|
|
43
|
-
/** npm 镜像源预设:空串 = 官方源;值相等即视为选中对应预设,其余进入「自定义」输入。 */
|
|
44
|
-
const MIRROR_PRESETS: Array<{ value: string; labelKey: string }> = [
|
|
45
|
-
{ value: '', labelKey: 'mirrorOfficial' },
|
|
46
|
-
{ value: 'https://registry.npmmirror.com', labelKey: 'mirrorNpmmirror' },
|
|
47
|
-
{ value: 'https://mirrors.cloud.tencent.com/npm/', labelKey: 'mirrorTencent' },
|
|
48
|
-
]
|
|
49
|
-
|
|
50
54
|
/** 设置行:默认标签左控件右;stack=true 时控件独占一行(文本框整行显示),hairline 分隔。 */
|
|
51
55
|
function SettingRow({ title, desc, children, stack = false }: {
|
|
52
56
|
title: string
|
|
@@ -76,13 +80,8 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
|
|
|
76
80
|
/** 恢复默认确认弹窗:点按钮先弹窗询问,确认后才真正 reset(不做两段式按钮) */
|
|
77
81
|
const [resetConfirm, setResetConfirm] = useState(false)
|
|
78
82
|
|
|
79
|
-
const mirrorCustom = !MIRROR_PRESETS.some((p) => p.value === settings.npmRegistry)
|
|
80
|
-
const onMirrorChange = (value: string) => {
|
|
81
|
-
if (value === 'custom') return // 进入自定义输入:保持当前值,下方出现输入框
|
|
82
|
-
update({ npmRegistry: value })
|
|
83
|
-
}
|
|
84
|
-
const onMirrorInput = (e: ChangeEvent<HTMLInputElement>) => update({ npmRegistry: e.target.value })
|
|
85
83
|
const onProxyChange = (e: ChangeEvent<HTMLInputElement>) => update({ proxy: e.target.value })
|
|
84
|
+
const onMirrorChange = (value: string) => update({ npmRegistry: value })
|
|
86
85
|
|
|
87
86
|
/** 每个分组的页面副标题(导航标题之上再补一句说明,保持桌面设置质感) */
|
|
88
87
|
const pageDesc: Record<SettingsSection, string> = {
|
|
@@ -106,31 +105,19 @@ export function SettingsView({ t, settings, update, reset, env, onCopy }: {
|
|
|
106
105
|
title: t('settingsCheckOnStart'),
|
|
107
106
|
}),
|
|
108
107
|
}),
|
|
108
|
+
// npm 镜像源:预设下拉(默认未配置跟随本机),选国内顶级镜像可加速安装;
|
|
109
|
+
// 不做自定义输入,其它镜像写在 ~/.npmrc 即可被天然尊重。
|
|
110
|
+
// 独占一行(同代理文本框):标签在上、下拉在下整行展开,长描述不会挤压控件
|
|
109
111
|
h(SettingRow, {
|
|
110
112
|
title: t('settingsMirror'),
|
|
111
113
|
desc: t('settingsMirrorDesc'),
|
|
114
|
+
stack: true,
|
|
112
115
|
children: h(Dropdown, {
|
|
113
|
-
value:
|
|
114
|
-
options:
|
|
115
|
-
...MIRROR_PRESETS.map((p) => ({ value: p.value, label: t(p.labelKey) })),
|
|
116
|
-
{ value: 'custom', label: t('mirrorCustom') },
|
|
117
|
-
],
|
|
116
|
+
value: settings.npmRegistry,
|
|
117
|
+
options: MIRROR_PRESETS.map((m) => ({ value: m.value, label: t(m.labelKey) })),
|
|
118
118
|
onChange: onMirrorChange,
|
|
119
119
|
title: t('settingsMirror'),
|
|
120
|
-
className: styles.controlDropdown
|
|
121
|
-
}),
|
|
122
|
-
}),
|
|
123
|
-
// 自定义镜像源:下拉选中「自定义」或已存非预设值时,追加一行输入框(文本框独占一行)
|
|
124
|
-
mirrorCustom && h(SettingRow, {
|
|
125
|
-
title: t('mirrorCustom'),
|
|
126
|
-
stack: true,
|
|
127
|
-
children: h('input', {
|
|
128
|
-
className: styles.textInput,
|
|
129
|
-
type: 'url',
|
|
130
|
-
value: settings.npmRegistry,
|
|
131
|
-
placeholder: 'https://registry.npmjs.org',
|
|
132
|
-
spellCheck: false,
|
|
133
|
-
onChange: onMirrorInput,
|
|
120
|
+
className: `${styles.controlDropdown} ${dropdownStyles.dropdownFill}`,
|
|
134
121
|
}),
|
|
135
122
|
}),
|
|
136
123
|
// 代理地址:文本框独占一行,整行输入,便于粘贴长代理地址
|