@noob-stupid/dsh-plugin-console 0.3.8 → 0.3.11

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
@@ -70,14 +70,18 @@ static plugin & skill index** refreshed by CI every 6 hours.
70
70
  The plugin declares a `dsh.bundle` manifest, so a single command installs and enables it:
71
71
 
72
72
  ```sh
73
+ # npm release (recommended: prebuilt, no git / build authorization needed)
74
+ dsh plugin --profile web add @noob-stupid/dsh-plugin-console
75
+
76
+ # or install from GitHub source (needs git; allowBuilds authorization on first add)
73
77
  dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
74
78
  ```
75
79
 
76
80
  Uninstall / reinstall (update):
77
81
 
78
82
  ```sh
79
- dsh plugin --profile web remove github:Noob-stupid/dsh-plugin-hub
80
- dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
83
+ dsh plugin --profile web remove @noob-stupid/dsh-plugin-console
84
+ dsh plugin --profile web add @noob-stupid/dsh-plugin-console
81
85
  ```
82
86
 
83
87
  Then restart the dsh service → refresh the page → Settings → Plugins → Plugin Console.
@@ -105,7 +109,7 @@ idempotently appends an enable entry to `cordis.patch.yml`. Afterwards:
105
109
 
106
110
  ### Option 3: hand it to an AI in one sentence
107
111
 
108
- > Install the DSH plugin hub (dsh-plugin-hub): run `dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub`; if there is no dsh CLI, clone https://github.com/Noob-stupid/dsh-plugin-hub to `~/.dsh/profiles/web/node_modules/` and register it in `cordis.patch.yml` (id: plugin-console, name: @deepseek-ai/dsh-plugin-console). Restart dsh web afterwards.
112
+ > Install the DSH plugin hub (dsh-plugin-hub): run `dsh plugin --profile web add @noob-stupid/dsh-plugin-console` (npm release); if there is no dsh CLI, clone https://github.com/Noob-stupid/dsh-plugin-hub to `~/.dsh/profiles/web/node_modules/` and register it in `cordis.patch.yml` (id: plugin-console, name: @noob-stupid/dsh-plugin-console). Restart dsh web afterwards.
109
113
 
110
114
  Requires: DSH ≥ 0.1.0-rc.6 (web profile, with `dsh-client-modules` / `dsh-host-plugin-inventory`).
111
115
 
package/README.zh.md CHANGED
@@ -64,14 +64,18 @@
64
64
  插件声明了 `dsh.bundle` 官方清单,一条命令装好并自动启用:
65
65
 
66
66
  ```sh
67
+ # npm 发布版(推荐:预构建产物,无需 git / 构建授权)
68
+ dsh plugin --profile web add @noob-stupid/dsh-plugin-console
69
+
70
+ # 或直接装 GitHub 源码(需 git,首次需 allowBuilds 授权)
67
71
  dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
68
72
  ```
69
73
 
70
74
  卸载 / 重装(即更新):
71
75
 
72
76
  ```sh
73
- dsh plugin --profile web remove github:Noob-stupid/dsh-plugin-hub
74
- dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub
77
+ dsh plugin --profile web remove @noob-stupid/dsh-plugin-console
78
+ dsh plugin --profile web add @noob-stupid/dsh-plugin-console
75
79
  ```
76
80
 
77
81
  然后重启 dsh 服务 → 刷新页面 → 设置 → 插件 → 插件管理。
@@ -98,7 +102,7 @@ git clone https://github.com/Noob-stupid/dsh-plugin-hub /tmp/dsh-plugin-console
98
102
 
99
103
  ### 方式三:一句话交给 AI
100
104
 
101
- > 安装 DSH 插件中心(dsh-plugin-hub):运行 `dsh plugin --profile web add github:Noob-stupid/dsh-plugin-hub`;若没有 dsh CLI,则克隆 https://github.com/Noob-stupid/dsh-plugin-hub 到 `~/.dsh/profiles/web/node_modules/`,在 `cordis.patch.yml` 注册(id: plugin-console,name: @deepseek-ai/dsh-plugin-console)。完成后重启 dsh web。
105
+ > 安装 DSH 插件中心(dsh-plugin-hub):运行 `dsh plugin --profile web add @noob-stupid/dsh-plugin-console`(npm 发布版);若没有 dsh CLI,则克隆 https://github.com/Noob-stupid/dsh-plugin-hub 到 `~/.dsh/profiles/web/node_modules/`,在 `cordis.patch.yml` 注册(id: plugin-console,name: @noob-stupid/dsh-plugin-console)。完成后重启 dsh web。
102
106
 
103
107
  要求:DSH ≥ 0.1.0-rc.6(web profile,含 `dsh-client-modules` / `dsh-host-plugin-inventory`)。
104
108
 
package/lib/client.js CHANGED
@@ -947,7 +947,7 @@ window.__ModuleLoader__.load({
947
947
  const [giteeClientSecret, setGiteeClientSecret] = react.useState("");
948
948
  // 派生:Gitee 登录状态
949
949
  const giteeStatus = sourcesData !== null
950
- ? { clientConfigured: !!sourcesData.gitee?.clientId, hasToken: !!sourcesData.gitee?.token, login: sourcesData.gitee?.login ?? "" }
950
+ ? { clientConfigured: !!sourcesData.gitee?.clientConfigured, hasToken: !!sourcesData.gitee?.hasToken, login: sourcesData.gitee?.login ?? "" }
951
951
  : null;
952
952
  // 搜索源(默认 GitHub;点击登录标切换),localStorage 持久化
953
953
  const [searchSource, setSearchSource] = react.useState(() => {
@@ -1024,12 +1024,25 @@ window.__ModuleLoader__.load({
1024
1024
  (data) => {
1025
1025
  if (data && data.error === null) {
1026
1026
  const current = state.data.compat?.dshVersion ?? "";
1027
- // 版本号比较(防 current 已是 rc.8 时误判 rc.7 为升级目标;rc.N 数字比较)
1028
- const verNum = (v) => { const m = String(v).match(/0\.1\.0-rc\.(\d+)/u); return m ? parseInt(m[1], 10) : -1; };
1027
+ // 通用 semver 比较(支持 0.1.0-rc.8 → 0.1.1-rc.2 跨 minor 升级;0.1.1-rc.N 不再写死 0.1.0)
1028
+ const verNum = (v) => {
1029
+ const m = String(v).match(/^(\d+)\.(\d+)\.(\d+)(?:-(?:[a-z]+\.)?(\d+))?$/iu);
1030
+ if (!m) return -1;
1031
+ const [ , maj, min, pat, rc ] = m;
1032
+ // 正式版(无 rc)视为 rc.∞:0.1.1 > 0.1.1-rc.2
1033
+ return { maj: parseInt(maj, 10), min: parseInt(min, 10), pat: parseInt(pat, 10), rc: rc === undefined ? Number.POSITIVE_INFINITY : parseInt(rc, 10) };
1034
+ };
1035
+ const isNewer = (a, b) => {
1036
+ if (a === -1 || b === -1) return false;
1037
+ if (a.maj !== b.maj) return a.maj > b.maj;
1038
+ if (a.min !== b.min) return a.min > b.min;
1039
+ if (a.pat !== b.pat) return a.pat > b.pat;
1040
+ return a.rc > b.rc;
1041
+ };
1029
1042
  // 稳定版 latest 优先;latest 不高于当前而 next(预发布渠道)更高时,目标取 next
1030
- const target = data.latest && verNum(data.latest) > verNum(current)
1043
+ const target = data.latest && isNewer(verNum(data.latest), verNum(current))
1031
1044
  ? data.latest
1032
- : (data.next && verNum(data.next) > verNum(current) ? data.next : null);
1045
+ : (data.next && isNewer(verNum(data.next), verNum(current)) ? data.next : null);
1033
1046
  if (target) setUpdateMap((prev) => ({ ...prev, [fwItem.fullName]: target }));
1034
1047
  }
1035
1048
  },
@@ -1474,17 +1487,25 @@ window.__ModuleLoader__.load({
1474
1487
  setSourcesData(data.sources);
1475
1488
  setSearchSourcesList(data.sources.searchSources ?? []);
1476
1489
  setGiteeClientId(data.sources.gitee?.clientId ?? "");
1477
- setGiteeClientSecret(data.sources.gitee?.clientSecret ?? "");
1490
+ // 安全:secret 永不回传(服务端已脱敏);已配置时留空、保存需重新输入,
1491
+ // 未配置时为空表单引导首次填写
1492
+ setGiteeClientSecret("");
1478
1493
  },
1479
1494
  (error) => { setSourcesOpen(false); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
1480
1495
  );
1481
1496
  };
1482
1497
  const saveGiteeSetup = () => {
1483
- if (!giteeClientId.trim() || !giteeClientSecret.trim()) {
1498
+ // clientId 打码回显(如 abc12345…):用户未修改时原样传回服务端,服务端识别打码标记保留原配置
1499
+ const idValue = giteeClientId.trim();
1500
+ if (!idValue || !giteeClientSecret.trim()) {
1484
1501
  setMessage(t("failed") + ":client_id / client_secret 不能为空");
1485
1502
  return;
1486
1503
  }
1487
- sourcesAction({ action: "gitee-setup", clientId: giteeClientId.trim(), clientSecret: giteeClientSecret.trim() });
1504
+ if (idValue.endsWith("…")) {
1505
+ sourcesAction({ action: "gitee-setup", clientId: "", clientSecret: giteeClientSecret.trim(), keepClientId: true });
1506
+ } else {
1507
+ sourcesAction({ action: "gitee-setup", clientId: idValue, clientSecret: giteeClientSecret.trim() });
1508
+ }
1488
1509
  };
1489
1510
  const startGiteeOauth = () => {
1490
1511
  call("/plugin-console/gitee-oauth-url").then(
package/lib/index.js CHANGED
@@ -52,6 +52,7 @@ const PROTECTED_MODULE_PATTERNS = [
52
52
  /^@deepseek-ai\/dsh-client-hmr$/u,
53
53
  /^@deepseek-ai\/dsh-client-runtime$/u,
54
54
  /^@deepseek-ai\/dsh-client-locale$/u,
55
+ /^@deepseek-ai\/dsh-client-ui-attachment$/u,
55
56
  /^@deepseek-ai\/dsh-client-web/u,
56
57
  /^@deepseek-ai\/dsh-web-frontend$/u,
57
58
  /^@deepseek-ai\/dsh-web-app$/u,
@@ -59,6 +60,7 @@ const PROTECTED_MODULE_PATTERNS = [
59
60
  /^@deepseek-ai\/dsh-credentials/u,
60
61
  /^@deepseek-ai\/dsh-session/u,
61
62
  /^@deepseek-ai\/dsh-storage/u,
63
+ /^@deepseek-ai\/dsh-attachment/u,
62
64
  /^@deepseek-ai\/dsh-typert/u,
63
65
  /^@deepseek-ai\/dsh-api-remotes$/u,
64
66
  /^@deepseek-ai\/dsh-tools$/u,
@@ -305,7 +307,7 @@ async function removeInsertRow(patchPath, rowId) {
305
307
  let next = text.replace(blockRe, '')
306
308
  const overrideRe = new RegExp(`^- id: ${escapeRegExp(rowId)}\\s*\\r?\\n {2}disabled: (true|false)\\s*\\r?\\n`, 'mu')
307
309
  next = next.replace(overrideRe, '')
308
- if (next !== text) await writeFile(patchPath, next, 'utf8')
310
+ if (next !== text) await writeFile(patchPath, sanitizePatchText(next), 'utf8')
309
311
  })
310
312
  }
311
313
 
@@ -468,6 +470,42 @@ async function writeSources(sources) {
468
470
  await writeFile(SOURCES_FILE, JSON.stringify(sources, null, 2) + '\n', 'utf8')
469
471
  }
470
472
 
473
+ /**
474
+ * 凭据脱敏(安全审查发现):/sources 响应不得携带明文密钥——
475
+ * - Gitee clientSecret / token:绝不回传(clientId 打码保留前 8 位供识别)
476
+ * - 自定义搜索源的 headers(可能含 Authorization: Bearer xxx):value 打码
477
+ * 前端需要"已配置"状态时用 giteeStatusView 的布尔字段。
478
+ */
479
+ function maskSources(sources) {
480
+ const gitee = readGiteeConfig(sources)
481
+ const maskedGitee = {
482
+ clientId: gitee.clientId === '' ? '' : `${gitee.clientId.slice(0, 8)}…`,
483
+ clientConfigured: gitee.clientId !== '',
484
+ hasToken: gitee.token !== '',
485
+ login: gitee.login,
486
+ }
487
+ return {
488
+ registries: sources.registries,
489
+ searchSources: (sources.searchSources ?? []).map((s) => {
490
+ if (s && typeof s.headers === 'object' && Object.keys(s.headers).length > 0) {
491
+ const masked = {}
492
+ for (const [k, v] of Object.entries(s.headers)) {
493
+ masked[k] = typeof v === 'string' && v.length > 8 ? `${v.slice(0, 4)}…${v.slice(-4)}` : (v === '' ? '' : '••••')
494
+ }
495
+ return { ...s, headers: masked }
496
+ }
497
+ return s
498
+ }),
499
+ gitee: maskedGitee,
500
+ }
501
+ }
502
+
503
+ /** Gitee 配置状态视图(布尔 + login,无任何凭据)。 */
504
+ function giteeStatusView(sources) {
505
+ const gitee = readGiteeConfig(sources)
506
+ return { clientConfigured: gitee.clientId !== '', hasToken: gitee.token !== '', login: gitee.login }
507
+ }
508
+
471
509
  /** 按主→备顺序返回 registry URL 列表。 */
472
510
  function orderedRegistries(sources) {
473
511
  const list = [...sources.registries]
@@ -758,12 +796,29 @@ function disableBlock(id) {
758
796
  return `- id: ${id}\n disabled: true\n`
759
797
  }
760
798
 
799
+ /**
800
+ * 清理补丁文件中的顶层空数组占位符(issue #7 事故教训):
801
+ * DSH profile 模板的 cordis.patch.yml 以注释 + 顶层 `[]` 占位符初始化(如 `# ...\n[]`)。
802
+ * 直接追加条目会生成 `[]` 后又跟 `- id: xxx` 的非法 YAML(同文档流里数组结束符 + 后续项),
803
+ * 导致 dsh 启动解析崩溃。写入前必须移除顶层独立的 `[]` / `[ ]` 占位行。
804
+ * 仅处理"整行就是空数组"的占位符;合法内容(如 `- insert:` 列表)不受影响。
805
+ */
806
+ function sanitizePatchText(text) {
807
+ return text
808
+ .split(/\r?\n/u)
809
+ .filter((line) => !/^\s*\[\s*\]\s*$/u.test(line))
810
+ .join('\n')
811
+ .replace(/\n{3,}/gu, '\n\n')
812
+ .replace(/\s+$/u, '') + '\n'
813
+ }
814
+
761
815
  /** 停用:追加 disabled:true 块(已存在则不动)。 */
762
816
  async function disableEntry(patchPath, id) {
763
817
  return queuedWrite(async () => {
764
818
  const { disables, text } = await readPatchState(patchPath)
765
819
  if (disables.includes(id)) return { changed: false }
766
- const next = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`
820
+ const clean = sanitizePatchText(text)
821
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
767
822
  await writeFile(patchPath, `${next}${disableBlock(id)}`, 'utf8')
768
823
  return { changed: true }
769
824
  })
@@ -775,11 +830,12 @@ async function enableEntry(patchPath, id) {
775
830
  const { disables, forced, text } = await readPatchState(patchPath)
776
831
  const blockRe = new RegExp(`^- id: ${escapeRegExp(id)}\\r?\\n disabled: true\\r?\\n`, 'mu')
777
832
  if (blockRe.test(text)) {
778
- await writeFile(patchPath, text.replace(blockRe, ''), 'utf8')
833
+ await writeFile(patchPath, sanitizePatchText(text.replace(blockRe, '')), 'utf8')
779
834
  return { changed: true }
780
835
  }
781
836
  if (forced.includes(id)) return { changed: false }
782
- const next = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`
837
+ const clean = sanitizePatchText(text)
838
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
783
839
  await writeFile(patchPath, `${next}- id: ${id}\n disabled: false\n`, 'utf8')
784
840
  return { changed: true }
785
841
  })
@@ -790,7 +846,8 @@ async function appendInsert(patchPath, entryId, packageName) {
790
846
  return queuedWrite(async () => {
791
847
  const { inserts, text } = await readPatchState(patchPath)
792
848
  if (inserts.includes(entryId)) return { changed: false }
793
- const next = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`
849
+ const clean = sanitizePatchText(text)
850
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
794
851
  const block = `- insert:\n - id: ${entryId}\n name: '${packageName}'\n`
795
852
  await writeFile(patchPath, `${next}${block}`, 'utf8')
796
853
  return { changed: true }
@@ -1241,31 +1298,44 @@ function githubRepoInfo(repo) {
1241
1298
  }
1242
1299
 
1243
1300
  /** raw 文件读取:https 主通道(含镜像)、gh CLI、curl 直连、curl jsDelivr CDN **四通道并行竞速**。
1244
- * 黑洞期 https/ghproxy 全挂、gh 可能不在服务进程 PATH——jsDelivr CDN 走系统网络且国内可达,是最后兜底。 */
1301
+ * 黑洞期 https/ghproxy 全挂、gh 可能不在服务进程 PATH——jsDelivr CDN 走系统网络且国内可达,是最后兜底。
1302
+ * 关键语义:**404 是确定性结果**(文件不存在,探测 .gitmodules/SKILL.md 等时的正常答案),
1303
+ * 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。 */
1245
1304
  async function rawTextWithFallback(repo, branch, path) {
1246
1305
  const url = `${GITHUB_RAW}/${repo}/${encodeURIComponent(branch)}/${path}`
1247
- const https = githubText(url)
1306
+ // 404/4xx:确定性"不存在",立即以 null 胜出(不等慢分支)
1307
+ const notFound = (promise) => promise
1308
+ .catch((error) => {
1309
+ if (error instanceof Error && /HTTP 404|HTTP 400|Not Found|not found|非 2xx|HTTP 非 2xx/u.test(error.message)) return null
1310
+ throw error
1311
+ })
1312
+ const https = notFound(githubText(url)
1248
1313
  .then((body) => {
1249
1314
  if (body === null) throw new Error('HTTP 非 2xx')
1250
1315
  return body
1251
- })
1252
- const gh = githubViaGh(`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`)
1316
+ }))
1317
+ const gh = notFound(githubViaGh(`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`)
1253
1318
  .then((data) => {
1254
1319
  if (data && typeof data.content === 'string') return Buffer.from(data.content, 'base64').toString('utf8')
1255
1320
  throw new Error('contents 无内容')
1256
- })
1257
- const curl = curlText(url, 10000)
1321
+ }))
1322
+ const curl = notFound(curlText(url, 6000)
1258
1323
  .then((body) => {
1259
1324
  if (body === null) throw new Error('HTTP 非 2xx')
1260
1325
  return body
1261
- })
1262
- const cdn = curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 10000)
1326
+ }))
1327
+ const cdn = notFound(curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 6000)
1263
1328
  .then((body) => {
1264
1329
  if (body === null) throw new Error('HTTP 非 2xx')
1265
1330
  return body
1266
- })
1331
+ }))
1267
1332
  try {
1268
- return await Promise.any([https, gh, curl, cdn])
1333
+ // 任一通道成功(含 404→null)立即返回;整体 5s 封顶(jsDelivr 正常 0.7s,
1334
+ // 黑洞期快速失败优于等 12s),超时视为不可达(null)。
1335
+ return await Promise.race([
1336
+ Promise.any([https, gh, curl, cdn]),
1337
+ new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
1338
+ ])
1269
1339
  } catch {
1270
1340
  return null
1271
1341
  }
@@ -1441,10 +1511,15 @@ function parseBundlePatchRefs(text) {
1441
1511
  * 捆绑依赖补装(事故教训:dsh-web-ui-all 更新后 17 个捆绑依赖缺失 → 服务加载崩溃):
1442
1512
  * curl/GitHub 通道只解压主包,这里逐个补装直接依赖(pnpm → curl 依次尝试)。
1443
1513
  * 返回仍缺失的依赖列表。
1514
+ * 安全护栏(事故教训):@deepseek-ai/* 框架内部包**绝不补装**——它们由框架依赖树管理
1515
+ * (正确版本随 @deepseek-ai/dsh 一起安装),npm 上这些内部包的 dist-tags.latest 是远古
1516
+ * 版本(如 dsh-host-webserver@0.0.1-rc.1),无版本约束补装会覆盖框架正确版本,
1517
+ * 导致 webServer 等服务起不来、整个 profile 启动崩溃。
1444
1518
  */
1445
1519
  async function backfillMissingDeps(profileDir, deps, registries) {
1446
1520
  const stillMissing = []
1447
1521
  for (const dep of deps) {
1522
+ if (/^@deepseek-ai\//u.test(dep)) continue // 框架内部包:跳过(宿主提供)
1448
1523
  if (existsSync(join(profileDir, 'node_modules', dep))) continue
1449
1524
  let ok = false
1450
1525
  try {
@@ -1453,8 +1528,10 @@ async function backfillMissingDeps(profileDir, deps, registries) {
1453
1528
  } catch {}
1454
1529
  if (!ok) {
1455
1530
  try {
1456
- const depInfo = await curlManualInstall(profileDir, dep, registries)
1457
- ok = depInfo.missingDeps.length === 0
1531
+ await curlManualInstall(profileDir, dep, registries)
1532
+ // 判断目标是否实际安装(修复:旧代码用 depInfo.missingDeps.length===0 判断
1533
+ // "该依赖自身无依赖",只要目标依赖带依赖就误报缺失——即使 curl 已成功安装)
1534
+ ok = existsSync(join(profileDir, 'node_modules', dep))
1458
1535
  } catch {}
1459
1536
  }
1460
1537
  if (!ok) stillMissing.push(dep)
@@ -1503,7 +1580,8 @@ async function githubReleaseInstall(profileDir, repo, packageName) {
1503
1580
  // 盒子实验:静态验证通过才覆盖(失败保留旧版本)。引用检查根用目标目录(宿主插件在根层)
1504
1581
  const box = verifyPackageBox(pkgPath, profileDir, packageName, targetRoot)
1505
1582
  const pkg = JSON.parse(readFileSync(join(pkgPath, 'package.json'), 'utf8'))
1506
- const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) }
1583
+ // 只统计 dependencies(peerDependencies 是宿主契约,不补装——见 curlManualInstall 注释)
1584
+ const deps = { ...(pkg.dependencies ?? {}) }
1507
1585
  const missingDeps = Object.keys(deps).filter((d) => !existsSync(join(profileDir, 'node_modules', d)))
1508
1586
  const target = join(targetRoot, packageName)
1509
1587
  if (existsSync(target)) rmSync(target, { recursive: true, force: true })
@@ -1564,6 +1642,9 @@ async function ensureBundlePatchIntegrity(profileDir, pkgName, userPatchPath) {
1564
1642
  if (refs.length === 0) return report
1565
1643
  const registries = orderedRegistries(readSources())
1566
1644
  for (const ref of refs) {
1645
+ // 框架内部包跳过(事故教训:npm 上 @deepseek-ai/* 的 dist-tags.latest 是远古版本,
1646
+ // 无版本约束补装会覆盖框架正确版本导致服务崩溃——见 backfillMissingDeps 注释)
1647
+ if (/^@deepseek-ai\//u.test(ref.name)) continue
1567
1648
  if (existsSync(join(profileDir, 'node_modules', ref.name))) continue
1568
1649
  let ok = false
1569
1650
  try {
@@ -1581,7 +1662,10 @@ async function ensureBundlePatchIntegrity(profileDir, pkgName, userPatchPath) {
1581
1662
  try {
1582
1663
  const userPatch = readFileSync(userPatchPath, 'utf8')
1583
1664
  if (!userPatch.includes(`id: ${ref.id}`)) {
1584
- appendFileSync(userPatchPath, `- id: ${ref.id}\n disabled: true\n`, 'utf8')
1665
+ // issue #7 防护:清理顶层 [] 占位符后再追加(模板初始文件直接追加会生成非法 YAML)
1666
+ const clean = sanitizePatchText(userPatch)
1667
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
1668
+ writeFileSync(userPatchPath, `${next}- id: ${ref.id}\n disabled: true\n`, 'utf8')
1585
1669
  report.disabled.push(ref.id)
1586
1670
  }
1587
1671
  } catch {}
@@ -2495,8 +2579,7 @@ async function handle(ctx, req, res) {
2495
2579
 
2496
2580
  if (method === 'GET' && pathname === `${ROUTE_PREFIX}/sources`) {
2497
2581
  const sources = readSources()
2498
- const gitee = readGiteeConfig(sources)
2499
- sendJson(res, 200, { ok: true, sources, giteeStatus: { clientConfigured: gitee.clientId !== '', hasToken: gitee.token !== '', login: gitee.login } })
2582
+ sendJson(res, 200, { ok: true, sources: maskSources(sources), giteeStatus: giteeStatusView(sources) })
2500
2583
  return
2501
2584
  }
2502
2585
 
@@ -2958,7 +3041,7 @@ async function handle(ctx, req, res) {
2958
3041
  }
2959
3042
  sources.registries.push(entry)
2960
3043
  await writeSources(sources)
2961
- sendJson(res, 200, { ok: true, sources })
3044
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
2962
3045
  return
2963
3046
  }
2964
3047
  if (action === 'remove') {
@@ -2985,7 +3068,7 @@ async function handle(ctx, req, res) {
2985
3068
  target.url = url
2986
3069
  target.name = name || url
2987
3070
  await writeSources(sources)
2988
- sendJson(res, 200, { ok: true, sources })
3071
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
2989
3072
  return
2990
3073
  }
2991
3074
  if (action === 'set-primary') {
@@ -2996,7 +3079,7 @@ async function handle(ctx, req, res) {
2996
3079
  }
2997
3080
  for (const r of sources.registries) r.primary = r.id === id
2998
3081
  await writeSources(sources)
2999
- sendJson(res, 200, { ok: true, sources })
3082
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3000
3083
  return
3001
3084
  }
3002
3085
  if (action === 'add-search') {
@@ -3028,7 +3111,7 @@ async function handle(ctx, req, res) {
3028
3111
  ...(headers.length > 0 ? { headers } : {}),
3029
3112
  })
3030
3113
  await writeSources(sources)
3031
- sendJson(res, 200, { ok: true, sources })
3114
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3032
3115
  return
3033
3116
  }
3034
3117
  if (action === 'remove-search') {
@@ -3044,33 +3127,35 @@ async function handle(ctx, req, res) {
3044
3127
  }
3045
3128
  sources.searchSources = sources.searchSources.filter((s) => s.id !== id)
3046
3129
  await writeSources(sources)
3047
- sendJson(res, 200, { ok: true, sources })
3130
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3048
3131
  return
3049
3132
  }
3050
3133
  if (action === 'reset') {
3051
3134
  const defaults = JSON.parse(JSON.stringify(DEFAULT_SOURCES))
3052
3135
  await writeSources(defaults)
3053
- sendJson(res, 200, { ok: true, sources: defaults })
3136
+ sendJson(res, 200, { ok: true, sources: maskSources(defaults) })
3054
3137
  return
3055
3138
  }
3056
3139
  if (action === 'gitee-setup') {
3057
3140
  const clientId = typeof body.clientId === 'string' ? body.clientId.trim() : ''
3058
3141
  const clientSecret = typeof body.clientSecret === 'string' ? body.clientSecret.trim() : ''
3059
- if (!clientId || !clientSecret) {
3142
+ const keepClientId = body.keepClientId === true
3143
+ if ((!keepClientId && !clientId) || !clientSecret) {
3060
3144
  sendError(res, 400, 'client_id 与 client_secret 不能为空')
3061
3145
  return
3062
3146
  }
3063
3147
  const current = readGiteeConfig(sources)
3064
- sources.gitee = { ...current, clientId, clientSecret }
3148
+ // keepClientId:前端回显的是打码 clientId(abc12345…),用户只改了 secret 时保留原配置
3149
+ sources.gitee = { ...current, clientId: keepClientId ? current.clientId : clientId, clientSecret }
3065
3150
  await writeSources(sources)
3066
- sendJson(res, 200, { ok: true, sources })
3151
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3067
3152
  return
3068
3153
  }
3069
3154
  if (action === 'gitee-clear') {
3070
3155
  const current = readGiteeConfig(sources)
3071
3156
  sources.gitee = { clientId: current.clientId, clientSecret: current.clientSecret, token: '', login: '' }
3072
3157
  await writeSources(sources)
3073
- sendJson(res, 200, { ok: true, sources })
3158
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3074
3159
  return
3075
3160
  }
3076
3161
  sendError(res, 400, '未知操作(add / edit / set-primary / remove / add-search / remove-search / gitee-setup / gitee-clear / reset)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noob-stupid/dsh-plugin-console",
3
- "version": "0.3.8",
3
+ "version": "0.3.11",
4
4
  "description": "DSH plugin management panel \u0026 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
5
  "repository": {
6
6
  "type": "git",