@noob-stupid/dsh-plugin-console 0.3.16 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -166,7 +166,7 @@ Requires: DSH ≥ 0.1.0-rc.6 (web profile, with `dsh-client-modules` / `dsh-host
166
166
  - **Real-time progress**: a `DSH-Upgrade` console window pops up showing live pnpm download
167
167
  progress; the in-panel progress card shows the waiting time;
168
168
  - **Upgrade protection**: failed installs **auto-rollback** (robocopy, backup verified before
169
- upgrade), version check catches fake success, 10-min hard timeout, stall detection
169
+ upgrade), version check catches fake success, 15-min hard timeout, stall detection
170
170
  (no debug-log updates → auto switch registry), global trap fallback, and 15-min stale-state
171
171
  cleanup — the framework is never left broken;
172
172
  - **pnpm channel**: npm-cli.js freezes at startup in the schtasks task environment (0-byte
package/README.zh.md CHANGED
@@ -152,7 +152,7 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
152
152
  - **实时进度**:升级时弹出 `DSH-Upgrade` 窗口,实时显示 pnpm 下载进度;面板内进度卡片
153
153
  同步显示等待时长;
154
154
  - **升级保护**:失败**自动回滚**(robocopy,升级前校验回滚点有效)、版本校验防假成功、
155
- 10 分钟硬超时、卡死检测(debug 日志无更新自动切换 registry)、全局异常兜底、
155
+ 15 分钟硬超时、卡死检测(debug 日志无更新自动切换 registry)、全局异常兜底、
156
156
  15 分钟残留状态清理——框架绝不处于损坏状态;
157
157
  - **pnpm 通道**:npm-cli.js 在 schtasks 任务环境启动即卡死(debug 日志 0 字节、网络请求
158
158
  都发不出);升级改用 `corepack pnpm`(实测 0.4s 秒启动、11.5s 装完 rc.8)+ **国内源
package/lib/index.js CHANGED
@@ -236,7 +236,6 @@ function postJsonUrl(url, formBody) {
236
236
  'user-agent': GITHUB_UA,
237
237
  accept: 'application/json',
238
238
  },
239
- rejectUnauthorized: false,
240
239
  timeout: 20000,
241
240
  }, (res) => {
242
241
  const chunks = []
@@ -377,6 +376,8 @@ function cleanupStalePackageDir(profileDir, packageName) {
377
376
 
378
377
  /** 软件源配置文件(registry 列表,安装链按主→备依次尝试)。 */
379
378
  const SOURCES_FILE = join(dshHome(), 'plugin-console-sources.json')
379
+ /** 敏感凭据单独落盘(避免与可分享配置混存):自定义搜索源 headers + Gitee secret/token。 */
380
+ const SOURCES_SECRETS_FILE = join(dshHome(), 'plugin-console-sources.secrets.json')
380
381
  const DEFAULT_SOURCES = {
381
382
  registries: [
382
383
  { id: 'npmmirror', name: 'npmmirror(国内镜像)', url: 'https://registry.npmmirror.com', primary: true },
@@ -425,6 +426,7 @@ function readSources() {
425
426
  const defaults = JSON.parse(JSON.stringify(DEFAULT_SOURCES))
426
427
  try {
427
428
  const data = JSON.parse(readFileSync(SOURCES_FILE, 'utf8'))
429
+ const secrets = readSourceSecrets()
428
430
  const registries = (Array.isArray(data.registries) ? data.registries : [])
429
431
  .filter((r) => r && typeof r.url === 'string' && isAllowedSourceUrl(r.url))
430
432
  .map((r) => ({
@@ -439,13 +441,18 @@ function readSources() {
439
441
  return { id: String(s.id), name: String(s.name ?? s.id), type: 'builtin' }
440
442
  }
441
443
  if (s && typeof s.url === 'string' && s.url.includes('{q}')) {
442
- const headers = {}
443
- if (Array.isArray(s.headers)) {
444
+ let headers = {}
445
+ const secretHeaders = secrets.headers?.[s.id]
446
+ if (secretHeaders && typeof secretHeaders === 'object') {
447
+ headers = { ...secretHeaders }
448
+ } else if (Array.isArray(s.headers)) {
444
449
  for (const h of s.headers) {
445
450
  if (h && typeof h.name === 'string' && h.name !== '' && typeof h.value === 'string') {
446
451
  headers[h.name] = h.value
447
452
  }
448
453
  }
454
+ } else if (s.headers && typeof s.headers === 'object') {
455
+ headers = { ...s.headers }
449
456
  }
450
457
  return {
451
458
  id: String(s.id ?? `search-${Math.random().toString(36).slice(2, 8)}`).slice(0, 40),
@@ -458,16 +465,58 @@ function readSources() {
458
465
  return null
459
466
  })
460
467
  .filter((s) => s !== null)
468
+ const giteeBase = readGiteeConfig(data)
469
+ const gitee = {
470
+ ...giteeBase,
471
+ clientSecret: secrets.gitee?.clientSecret ?? giteeBase.clientSecret,
472
+ token: secrets.gitee?.token ?? giteeBase.token,
473
+ }
461
474
  if (registries.length > 0) {
462
475
  if (!registries.some((r) => r.primary)) registries[0].primary = true
463
- return { registries, searchSources, gitee: readGiteeConfig(data) }
476
+ return { registries, searchSources, gitee }
464
477
  }
465
478
  } catch {}
466
479
  return defaults
467
480
  }
468
481
 
482
+ function readSourceSecrets() {
483
+ try {
484
+ const data = JSON.parse(readFileSync(SOURCES_SECRETS_FILE, 'utf8'))
485
+ return data && typeof data === 'object' ? data : {}
486
+ } catch {
487
+ return {}
488
+ }
489
+ }
490
+
491
+ async function writeSourceSecrets(secrets) {
492
+ const gitee = secrets.gitee ?? {}
493
+ const headers = secrets.headers ?? {}
494
+ const hasGitee = typeof gitee.clientSecret === 'string' && gitee.clientSecret !== '' || typeof gitee.token === 'string' && gitee.token !== ''
495
+ const hasHeaders = Object.keys(headers).some((id) => { const h = headers[id]; return h && typeof h === 'object' && Object.keys(h).length > 0 })
496
+ if (!hasGitee && !hasHeaders) {
497
+ try { rmSync(SOURCES_SECRETS_FILE, { force: true }) } catch {}
498
+ return
499
+ }
500
+ await writeFile(SOURCES_SECRETS_FILE, JSON.stringify({ gitee, headers }, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
501
+ }
469
502
  async function writeSources(sources) {
470
- await writeFile(SOURCES_FILE, JSON.stringify(sources, null, 2) + '\n', 'utf8')
503
+ const secrets = { gitee: {}, headers: {} }
504
+ const cleanSearch = (sources.searchSources ?? []).map((s) => {
505
+ if (!s) return s
506
+ if (s.headers && typeof s.headers === 'object' && Object.keys(s.headers).length > 0) {
507
+ secrets.headers[s.id] = { ...s.headers }
508
+ const { headers, ...rest } = s
509
+ return rest
510
+ }
511
+ return s
512
+ })
513
+ const gitee = readGiteeConfig(sources)
514
+ if (typeof gitee.clientSecret === 'string' && gitee.clientSecret !== '') secrets.gitee.clientSecret = gitee.clientSecret
515
+ if (typeof gitee.token === 'string' && gitee.token !== '') secrets.gitee.token = gitee.token
516
+ const cleanGitee = { ...(sources.gitee ?? {}), clientSecret: undefined, token: undefined }
517
+ const main = { ...sources, searchSources: cleanSearch, gitee: cleanGitee }
518
+ await writeFile(SOURCES_FILE, JSON.stringify(main, null, 2) + '\n', 'utf8')
519
+ await writeSourceSecrets(secrets)
471
520
  }
472
521
 
473
522
  /**
@@ -900,6 +949,25 @@ function isLoopback(address) {
900
949
  return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
901
950
  }
902
951
 
952
+ /** 写请求跨站防护:浏览器同源策略不阻止跨站发送,必须校验 Origin / Sec-Fetch-Site。
953
+ * 非浏览器客户端(无 Origin/Sec-Fetch-Site 头)仍允许,避免 curl/脚本/测试被误伤。 */
954
+ function isAllowedWriteOrigin(req, port) {
955
+ const origin = typeof req.headers?.origin === 'string' ? req.headers.origin : null
956
+ const site = typeof req.headers?.['sec-fetch-site'] === 'string' ? req.headers['sec-fetch-site'] : null
957
+ if (origin !== null && origin !== '' && !ALLOWED_LOCAL_ORIGINS(port).has(origin)) return false
958
+ if (site !== null && site !== 'same-origin' && site !== 'none') return false
959
+ return true
960
+ }
961
+
962
+ function ALLOWED_LOCAL_ORIGINS(port) {
963
+ return new Set([
964
+ `http://127.0.0.1:${port}`,
965
+ `http://localhost:${port}`,
966
+ `http://[::1]:${port}`,
967
+ ])
968
+ }
969
+
970
+
903
971
  function sendJson(res, status, body) {
904
972
  const payload = JSON.stringify(body)
905
973
  res.writeHead(status, {
@@ -939,7 +1007,6 @@ function githubRequestOnce(url, { signal, accept, token, timeout = 20000 } = {})
939
1007
  accept: accept ?? 'application/vnd.github+json',
940
1008
  ...(token ? { authorization: `Bearer ${token}` } : {}),
941
1009
  },
942
- rejectUnauthorized: false,
943
1010
  }, resolve)
944
1011
  req.on('error', reject)
945
1012
  req.setTimeout(timeout, () => req.destroy(new Error('GitHub 请求超时')))
@@ -1124,7 +1191,7 @@ async function githubText(url, signal, token = null) {
1124
1191
  /** 用 curl 子进程请求 JSON(绕过本机中间设备对 node:https TLS 指纹的拦截;curl 走系统网络栈)。 */
1125
1192
  async function curlJson(url, timeout = 15000, headers = {}) {
1126
1193
  const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
1127
- const args = ['-s', '-m', String(Math.max(5, Math.ceil(timeout / 1000))), '--insecure', '-H', 'accept: application/json', '-w', '\n__HTTP__%{http_code}']
1194
+ const args = ['-s', '-m', String(Math.max(5, Math.ceil(timeout / 1000))), '-H', 'accept: application/json', '-w', '\n__HTTP__%{http_code}']
1128
1195
  for (const [key, value] of Object.entries(headers)) args.push('-H', `${key}: ${value}`)
1129
1196
  args.push(url)
1130
1197
  const { stdout } = await execFileAsync(bin, args, { timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 })
@@ -1140,7 +1207,7 @@ async function curlText(url, timeout = 10000) {
1140
1207
  const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
1141
1208
  const { stdout } = await execFileAsync(
1142
1209
  bin,
1143
- ['-s', '-m', String(Math.max(3, Math.ceil(timeout / 1000))), '--insecure', '-H', 'accept: text/plain', '-w', '\n__HTTP__%{http_code}', url],
1210
+ ['-s', '-m', String(Math.max(3, Math.ceil(timeout / 1000))), '-H', 'accept: text/plain', '-w', '\n__HTTP__%{http_code}', url],
1144
1211
  { timeout: timeout + 3000, windowsHide: true, maxBuffer: 8 * 1024 * 1024 },
1145
1212
  )
1146
1213
  const m = String(stdout).match(/\n__HTTP__(\d+)\s*$/u)
@@ -2559,6 +2626,18 @@ export function apply(ctx) {
2559
2626
  sendError(res, 403, '仅允许本机访问')
2560
2627
  return
2561
2628
  }
2629
+ const port = webPort(ctx)
2630
+ const host = typeof req.headers?.host === 'string' ? req.headers.host : ''
2631
+ if (![`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`].includes(host)) {
2632
+ sendError(res, 403, 'Host 校验失败(仅允许本机访问)')
2633
+ return
2634
+ }
2635
+ // 安全护栏(issue #9):写路由必须是本机同源请求,防止恶意网页跨站驱动敏感操作。
2636
+ const method = req.method ?? 'GET'
2637
+ if (method !== 'GET' && method !== 'HEAD' && !isAllowedWriteOrigin(req, port)) {
2638
+ sendError(res, 403, '跨站请求被拒绝(Origin/Sec-Fetch-Site 校验)')
2639
+ return
2640
+ }
2562
2641
  try {
2563
2642
  await handle(ctx, req, res)
2564
2643
  } catch (error) {
@@ -3445,18 +3524,18 @@ async function handle(ctx, req, res) {
3445
3524
  `Set-Location -Path ${ps(dirname(dshDir))}`,
3446
3525
  // npm 安装函数:Start-Process 跑 npm-cli.js(唯一正确入口——lib/cli.js 是函数模块,
3447
3526
  // 直跑只加载不执行,假成功 exit 0)+ --force(显式指定版本但已在 package.json 的
3448
- // semver 范围内时 npm 会跳过安装,假成功)+ 10 分钟总时长硬超时。注意:**不能加
3527
+ // semver 范围内时 npm 会跳过安装,假成功)+ 15 分钟总时长硬超时。注意:**不能加
3449
3528
  // -RedirectStandardOutput/-RedirectStandardError**——schtasks 任务环境下带重定向的
3450
3529
  // Start-Process 子进程会启动即异常退出(ExitCode 读不到、输出 0 字节,实测);
3451
3530
  // 无重定向时 ExitCode 正常(实测 exit=0)。npm 自身 debug 日志(cache\_logs)留档。
3452
- // 超时策略:总时长 10 分钟硬上限 + npm debug 日志 5 分钟无更新判定卡死(网络黑洞/挂起时
3453
- // 快速失败换 registry,不用干等 10 分钟);等待期间每 15 秒更新进度状态(客户端可见)。
3531
+ // 超时策略:总时长 15 分钟硬上限 + npm debug 日志 5 分钟无更新判定卡死(网络黑洞/挂起时
3532
+ // 快速失败换 registry,不用干等 15 分钟);等待期间每 15 秒更新进度状态(客户端可见)。
3454
3533
  // 成功后校验 package.json 版本 === target(防 npm 假成功)。
3455
3534
  // 另注意:npm install 在 schtasks 环境启动慢(约 1-2 分钟 0 日志,初始化/编译缓存),
3456
3535
  // 属正常,5 分钟卡死阈值不会误杀。
3457
3536
  `function Install-Framework($reg) {`,
3458
3537
  ` $startAt = Get-Date`,
3459
- ` $deadline = $startAt.AddMinutes(10)`,
3538
+ ` $deadline = $startAt.AddMinutes(15)`,
3460
3539
  ` $stallDeadline = $startAt.AddMinutes(5)`,
3461
3540
  ` $lastLogM = (Get-Date)`,
3462
3541
  ` $logSeen = $false`,
@@ -3493,7 +3572,7 @@ async function handle(ctx, req, res) {
3493
3572
  ` }`,
3494
3573
  ` }`,
3495
3574
  ` if (-not $done) {`,
3496
- ` Log 'pnpm 超过 10 分钟未完成,判定超时'`,
3575
+ ` Log 'pnpm 超过 15 分钟未完成,判定超时'`,
3497
3576
  ` try { taskkill /F /FI "WINDOWTITLE eq DSH-Upgrade" 2>$null | Out-Null } catch {}`,
3498
3577
  ` return 1`,
3499
3578
  ` }`,
@@ -3609,10 +3688,10 @@ async function handle(ctx, req, res) {
3609
3688
  ` $tgz = Join-Path $tmpDir 'pkg.tgz'`,
3610
3689
  ` $encName = $name.Replace('/','%2f')`,
3611
3690
  ` $url = 'https://registry.npmmirror.com/' + $encName + '/-/' + $short + '-' + $refVersion + '.tgz'`,
3612
- ` curl.exe -s -L -m 60 --insecure -o $tgz $url`,
3691
+ ` curl.exe -s -L -m 60 -o $tgz $url`,
3613
3692
  ` if (-not (Test-Path $tgz) -or (Get-Item $tgz).Length -lt 1000) {`,
3614
3693
  ` $url = 'https://registry.npmjs.org/' + $encName + '/-/' + $short + '-' + $refVersion + '.tgz'`,
3615
- ` curl.exe -s -L -m 60 --insecure -o $tgz $url`,
3694
+ ` curl.exe -s -L -m 60 -o $tgz $url`,
3616
3695
  ` }`,
3617
3696
  ` if (-not (Test-Path $tgz) -or (Get-Item $tgz).Length -lt 1000) { Remove-Item $tmpDir -Recurse -Force -ErrorAction SilentlyContinue; continue }`,
3618
3697
  ` tar -xzf $tgz -C $tmpDir`,
package/package.json CHANGED
@@ -1,39 +1,39 @@
1
- {
2
- "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.3.16",
4
- "description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/Noob-stupid/dsh-plugin-hub"
8
- },
9
- "type": "module",
10
- "main": "./lib/index.js",
11
- "exports": {
12
- ".": "./lib/index.js",
13
- "./client": "./lib/client.js",
14
- "./package.json": "./package.json"
15
- },
16
- "keywords": [
17
- "dsh-plugin",
18
- "deepseek-harness",
19
- "plugin-manager"
20
- ],
21
- "dsh": {
22
- "bundle": {
23
- "patch": "./cordis.patch.yml"
24
- },
25
- "client": {
26
- "platform": "web",
27
- "inject": [
28
- "@deepseek-ai/dsh-client-runtime",
29
- "@deepseek-ai/dsh-client-locale",
30
- "@deepseek-ai/dsh-client-ui-settings"
31
- ]
32
- }
33
- },
34
- "files": [
35
- "lib",
36
- "cordis.patch.yml"
37
- ],
38
- "license": "MIT"
39
- }
1
+ {
2
+ "name": "@noob-stupid/dsh-plugin-console",
3
+ "version": "0.3.18",
4
+ "description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Noob-stupid/dsh-plugin-hub"
8
+ },
9
+ "type": "module",
10
+ "main": "./lib/index.js",
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
+ "./client": "./lib/client.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "keywords": [
17
+ "dsh-plugin",
18
+ "deepseek-harness",
19
+ "plugin-manager"
20
+ ],
21
+ "dsh": {
22
+ "bundle": {
23
+ "patch": "./cordis.patch.yml"
24
+ },
25
+ "client": {
26
+ "platform": "web",
27
+ "inject": [
28
+ "@deepseek-ai/dsh-client-runtime",
29
+ "@deepseek-ai/dsh-client-locale",
30
+ "@deepseek-ai/dsh-client-ui-settings"
31
+ ]
32
+ }
33
+ },
34
+ "files": [
35
+ "lib",
36
+ "cordis.patch.yml"
37
+ ],
38
+ "license": "MIT"
39
+ }