@noob-stupid/dsh-plugin-console 0.3.9 → 0.3.12

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(() => {
@@ -1487,17 +1487,25 @@ window.__ModuleLoader__.load({
1487
1487
  setSourcesData(data.sources);
1488
1488
  setSearchSourcesList(data.sources.searchSources ?? []);
1489
1489
  setGiteeClientId(data.sources.gitee?.clientId ?? "");
1490
- setGiteeClientSecret(data.sources.gitee?.clientSecret ?? "");
1490
+ // 安全:secret 永不回传(服务端已脱敏);已配置时留空、保存需重新输入,
1491
+ // 未配置时为空表单引导首次填写
1492
+ setGiteeClientSecret("");
1491
1493
  },
1492
1494
  (error) => { setSourcesOpen(false); setMessage(t("failed") + ":" + friendlyGithubError(error).message); },
1493
1495
  );
1494
1496
  };
1495
1497
  const saveGiteeSetup = () => {
1496
- if (!giteeClientId.trim() || !giteeClientSecret.trim()) {
1498
+ // clientId 打码回显(如 abc12345…):用户未修改时原样传回服务端,服务端识别打码标记保留原配置
1499
+ const idValue = giteeClientId.trim();
1500
+ if (!idValue || !giteeClientSecret.trim()) {
1497
1501
  setMessage(t("failed") + ":client_id / client_secret 不能为空");
1498
1502
  return;
1499
1503
  }
1500
- 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
+ }
1501
1509
  };
1502
1510
  const startGiteeOauth = () => {
1503
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]
@@ -513,7 +551,8 @@ function readGithubAuth() {
513
551
  * 因此面板披露版本并给出提示,而不是默默失效。
514
552
  */
515
553
  const CONSOLE_VERSION = '0.1.0'
516
- const SUPPORTED_WEB_APP_PATTERN = /^0\.1\.0-/u
554
+ // 支持 0.1.x 系列(0.1.0 / 0.1.1 等);0.2 / 1.0 等破坏性大版本才标记不支持
555
+ const SUPPORTED_WEB_APP_PATTERN = /^0\.1\.\d+/u
517
556
 
518
557
  async function detectCompat(baseUrl) {
519
558
  const result = { consoleVersion: CONSOLE_VERSION, webAppVersion: null, dshVersion: null, supported: true, notice: null }
@@ -539,7 +578,7 @@ async function detectCompat(baseUrl) {
539
578
  }
540
579
  if (result.webAppVersion !== null && !SUPPORTED_WEB_APP_PATTERN.test(result.webAppVersion)) {
541
580
  result.supported = false
542
- result.notice = `当前 DSH web 包版本 ${result.webAppVersion} 不在受支持的 0.1.0 系列内,插件控制台的部分功能可能因官方破坏性更新而失效;请到 https://github.com/Noob-stupid/dsh-plugin-hub 获取匹配的更新`
581
+ result.notice = `当前 DSH web 包版本 ${result.webAppVersion} 不在受支持的 0.1.x 系列内,插件控制台的部分功能可能因官方破坏性更新而失效;请到 https://github.com/Noob-stupid/dsh-plugin-hub 获取匹配的更新`
543
582
  }
544
583
  return result
545
584
  }
@@ -758,12 +797,29 @@ function disableBlock(id) {
758
797
  return `- id: ${id}\n disabled: true\n`
759
798
  }
760
799
 
800
+ /**
801
+ * 清理补丁文件中的顶层空数组占位符(issue #7 事故教训):
802
+ * DSH profile 模板的 cordis.patch.yml 以注释 + 顶层 `[]` 占位符初始化(如 `# ...\n[]`)。
803
+ * 直接追加条目会生成 `[]` 后又跟 `- id: xxx` 的非法 YAML(同文档流里数组结束符 + 后续项),
804
+ * 导致 dsh 启动解析崩溃。写入前必须移除顶层独立的 `[]` / `[ ]` 占位行。
805
+ * 仅处理"整行就是空数组"的占位符;合法内容(如 `- insert:` 列表)不受影响。
806
+ */
807
+ function sanitizePatchText(text) {
808
+ return text
809
+ .split(/\r?\n/u)
810
+ .filter((line) => !/^\s*\[\s*\]\s*$/u.test(line))
811
+ .join('\n')
812
+ .replace(/\n{3,}/gu, '\n\n')
813
+ .replace(/\s+$/u, '') + '\n'
814
+ }
815
+
761
816
  /** 停用:追加 disabled:true 块(已存在则不动)。 */
762
817
  async function disableEntry(patchPath, id) {
763
818
  return queuedWrite(async () => {
764
819
  const { disables, text } = await readPatchState(patchPath)
765
820
  if (disables.includes(id)) return { changed: false }
766
- const next = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`
821
+ const clean = sanitizePatchText(text)
822
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
767
823
  await writeFile(patchPath, `${next}${disableBlock(id)}`, 'utf8')
768
824
  return { changed: true }
769
825
  })
@@ -775,11 +831,12 @@ async function enableEntry(patchPath, id) {
775
831
  const { disables, forced, text } = await readPatchState(patchPath)
776
832
  const blockRe = new RegExp(`^- id: ${escapeRegExp(id)}\\r?\\n disabled: true\\r?\\n`, 'mu')
777
833
  if (blockRe.test(text)) {
778
- await writeFile(patchPath, text.replace(blockRe, ''), 'utf8')
834
+ await writeFile(patchPath, sanitizePatchText(text.replace(blockRe, '')), 'utf8')
779
835
  return { changed: true }
780
836
  }
781
837
  if (forced.includes(id)) return { changed: false }
782
- const next = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`
838
+ const clean = sanitizePatchText(text)
839
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
783
840
  await writeFile(patchPath, `${next}- id: ${id}\n disabled: false\n`, 'utf8')
784
841
  return { changed: true }
785
842
  })
@@ -790,7 +847,8 @@ async function appendInsert(patchPath, entryId, packageName) {
790
847
  return queuedWrite(async () => {
791
848
  const { inserts, text } = await readPatchState(patchPath)
792
849
  if (inserts.includes(entryId)) return { changed: false }
793
- const next = text.length === 0 || text.endsWith('\n') ? text : `${text}\n`
850
+ const clean = sanitizePatchText(text)
851
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
794
852
  const block = `- insert:\n - id: ${entryId}\n name: '${packageName}'\n`
795
853
  await writeFile(patchPath, `${next}${block}`, 'utf8')
796
854
  return { changed: true }
@@ -1241,31 +1299,44 @@ function githubRepoInfo(repo) {
1241
1299
  }
1242
1300
 
1243
1301
  /** raw 文件读取:https 主通道(含镜像)、gh CLI、curl 直连、curl jsDelivr CDN **四通道并行竞速**。
1244
- * 黑洞期 https/ghproxy 全挂、gh 可能不在服务进程 PATH——jsDelivr CDN 走系统网络且国内可达,是最后兜底。 */
1302
+ * 黑洞期 https/ghproxy 全挂、gh 可能不在服务进程 PATH——jsDelivr CDN 走系统网络且国内可达,是最后兜底。
1303
+ * 关键语义:**404 是确定性结果**(文件不存在,探测 .gitmodules/SKILL.md 等时的正常答案),
1304
+ * 必须立即返回 null,不能等其它分支(黑洞期 https 通道 20s+ 才失败会拖死整个请求)。 */
1245
1305
  async function rawTextWithFallback(repo, branch, path) {
1246
1306
  const url = `${GITHUB_RAW}/${repo}/${encodeURIComponent(branch)}/${path}`
1247
- const https = githubText(url)
1307
+ // 404/4xx:确定性"不存在",立即以 null 胜出(不等慢分支)
1308
+ const notFound = (promise) => promise
1309
+ .catch((error) => {
1310
+ if (error instanceof Error && /HTTP 404|HTTP 400|Not Found|not found|非 2xx|HTTP 非 2xx/u.test(error.message)) return null
1311
+ throw error
1312
+ })
1313
+ const https = notFound(githubText(url)
1248
1314
  .then((body) => {
1249
1315
  if (body === null) throw new Error('HTTP 非 2xx')
1250
1316
  return body
1251
- })
1252
- const gh = githubViaGh(`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`)
1317
+ }))
1318
+ const gh = notFound(githubViaGh(`repos/${repo}/contents/${path}?ref=${encodeURIComponent(branch)}`)
1253
1319
  .then((data) => {
1254
1320
  if (data && typeof data.content === 'string') return Buffer.from(data.content, 'base64').toString('utf8')
1255
1321
  throw new Error('contents 无内容')
1256
- })
1257
- const curl = curlText(url, 10000)
1322
+ }))
1323
+ const curl = notFound(curlText(url, 6000)
1258
1324
  .then((body) => {
1259
1325
  if (body === null) throw new Error('HTTP 非 2xx')
1260
1326
  return body
1261
- })
1262
- const cdn = curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 10000)
1327
+ }))
1328
+ const cdn = notFound(curlText(`https://cdn.jsdelivr.net/gh/${repo}@${encodeURIComponent(branch)}/${path}`, 6000)
1263
1329
  .then((body) => {
1264
1330
  if (body === null) throw new Error('HTTP 非 2xx')
1265
1331
  return body
1266
- })
1332
+ }))
1267
1333
  try {
1268
- return await Promise.any([https, gh, curl, cdn])
1334
+ // 任一通道成功(含 404→null)立即返回;整体 5s 封顶(jsDelivr 正常 0.7s,
1335
+ // 黑洞期快速失败优于等 12s),超时视为不可达(null)。
1336
+ return await Promise.race([
1337
+ Promise.any([https, gh, curl, cdn]),
1338
+ new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
1339
+ ])
1269
1340
  } catch {
1270
1341
  return null
1271
1342
  }
@@ -1441,10 +1512,15 @@ function parseBundlePatchRefs(text) {
1441
1512
  * 捆绑依赖补装(事故教训:dsh-web-ui-all 更新后 17 个捆绑依赖缺失 → 服务加载崩溃):
1442
1513
  * curl/GitHub 通道只解压主包,这里逐个补装直接依赖(pnpm → curl 依次尝试)。
1443
1514
  * 返回仍缺失的依赖列表。
1515
+ * 安全护栏(事故教训):@deepseek-ai/* 框架内部包**绝不补装**——它们由框架依赖树管理
1516
+ * (正确版本随 @deepseek-ai/dsh 一起安装),npm 上这些内部包的 dist-tags.latest 是远古
1517
+ * 版本(如 dsh-host-webserver@0.0.1-rc.1),无版本约束补装会覆盖框架正确版本,
1518
+ * 导致 webServer 等服务起不来、整个 profile 启动崩溃。
1444
1519
  */
1445
1520
  async function backfillMissingDeps(profileDir, deps, registries) {
1446
1521
  const stillMissing = []
1447
1522
  for (const dep of deps) {
1523
+ if (/^@deepseek-ai\//u.test(dep)) continue // 框架内部包:跳过(宿主提供)
1448
1524
  if (existsSync(join(profileDir, 'node_modules', dep))) continue
1449
1525
  let ok = false
1450
1526
  try {
@@ -1453,8 +1529,10 @@ async function backfillMissingDeps(profileDir, deps, registries) {
1453
1529
  } catch {}
1454
1530
  if (!ok) {
1455
1531
  try {
1456
- const depInfo = await curlManualInstall(profileDir, dep, registries)
1457
- ok = depInfo.missingDeps.length === 0
1532
+ await curlManualInstall(profileDir, dep, registries)
1533
+ // 判断目标是否实际安装(修复:旧代码用 depInfo.missingDeps.length===0 判断
1534
+ // "该依赖自身无依赖",只要目标依赖带依赖就误报缺失——即使 curl 已成功安装)
1535
+ ok = existsSync(join(profileDir, 'node_modules', dep))
1458
1536
  } catch {}
1459
1537
  }
1460
1538
  if (!ok) stillMissing.push(dep)
@@ -1503,7 +1581,8 @@ async function githubReleaseInstall(profileDir, repo, packageName) {
1503
1581
  // 盒子实验:静态验证通过才覆盖(失败保留旧版本)。引用检查根用目标目录(宿主插件在根层)
1504
1582
  const box = verifyPackageBox(pkgPath, profileDir, packageName, targetRoot)
1505
1583
  const pkg = JSON.parse(readFileSync(join(pkgPath, 'package.json'), 'utf8'))
1506
- const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) }
1584
+ // 只统计 dependencies(peerDependencies 是宿主契约,不补装——见 curlManualInstall 注释)
1585
+ const deps = { ...(pkg.dependencies ?? {}) }
1507
1586
  const missingDeps = Object.keys(deps).filter((d) => !existsSync(join(profileDir, 'node_modules', d)))
1508
1587
  const target = join(targetRoot, packageName)
1509
1588
  if (existsSync(target)) rmSync(target, { recursive: true, force: true })
@@ -1564,6 +1643,9 @@ async function ensureBundlePatchIntegrity(profileDir, pkgName, userPatchPath) {
1564
1643
  if (refs.length === 0) return report
1565
1644
  const registries = orderedRegistries(readSources())
1566
1645
  for (const ref of refs) {
1646
+ // 框架内部包跳过(事故教训:npm 上 @deepseek-ai/* 的 dist-tags.latest 是远古版本,
1647
+ // 无版本约束补装会覆盖框架正确版本导致服务崩溃——见 backfillMissingDeps 注释)
1648
+ if (/^@deepseek-ai\//u.test(ref.name)) continue
1567
1649
  if (existsSync(join(profileDir, 'node_modules', ref.name))) continue
1568
1650
  let ok = false
1569
1651
  try {
@@ -1581,7 +1663,10 @@ async function ensureBundlePatchIntegrity(profileDir, pkgName, userPatchPath) {
1581
1663
  try {
1582
1664
  const userPatch = readFileSync(userPatchPath, 'utf8')
1583
1665
  if (!userPatch.includes(`id: ${ref.id}`)) {
1584
- appendFileSync(userPatchPath, `- id: ${ref.id}\n disabled: true\n`, 'utf8')
1666
+ // issue #7 防护:清理顶层 [] 占位符后再追加(模板初始文件直接追加会生成非法 YAML)
1667
+ const clean = sanitizePatchText(userPatch)
1668
+ const next = clean.length === 0 || clean.endsWith('\n') ? clean : `${clean}\n`
1669
+ writeFileSync(userPatchPath, `${next}- id: ${ref.id}\n disabled: true\n`, 'utf8')
1585
1670
  report.disabled.push(ref.id)
1586
1671
  }
1587
1672
  } catch {}
@@ -2495,8 +2580,7 @@ async function handle(ctx, req, res) {
2495
2580
 
2496
2581
  if (method === 'GET' && pathname === `${ROUTE_PREFIX}/sources`) {
2497
2582
  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 } })
2583
+ sendJson(res, 200, { ok: true, sources: maskSources(sources), giteeStatus: giteeStatusView(sources) })
2500
2584
  return
2501
2585
  }
2502
2586
 
@@ -2958,7 +3042,7 @@ async function handle(ctx, req, res) {
2958
3042
  }
2959
3043
  sources.registries.push(entry)
2960
3044
  await writeSources(sources)
2961
- sendJson(res, 200, { ok: true, sources })
3045
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
2962
3046
  return
2963
3047
  }
2964
3048
  if (action === 'remove') {
@@ -2985,7 +3069,7 @@ async function handle(ctx, req, res) {
2985
3069
  target.url = url
2986
3070
  target.name = name || url
2987
3071
  await writeSources(sources)
2988
- sendJson(res, 200, { ok: true, sources })
3072
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
2989
3073
  return
2990
3074
  }
2991
3075
  if (action === 'set-primary') {
@@ -2996,7 +3080,7 @@ async function handle(ctx, req, res) {
2996
3080
  }
2997
3081
  for (const r of sources.registries) r.primary = r.id === id
2998
3082
  await writeSources(sources)
2999
- sendJson(res, 200, { ok: true, sources })
3083
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3000
3084
  return
3001
3085
  }
3002
3086
  if (action === 'add-search') {
@@ -3028,7 +3112,7 @@ async function handle(ctx, req, res) {
3028
3112
  ...(headers.length > 0 ? { headers } : {}),
3029
3113
  })
3030
3114
  await writeSources(sources)
3031
- sendJson(res, 200, { ok: true, sources })
3115
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3032
3116
  return
3033
3117
  }
3034
3118
  if (action === 'remove-search') {
@@ -3044,33 +3128,35 @@ async function handle(ctx, req, res) {
3044
3128
  }
3045
3129
  sources.searchSources = sources.searchSources.filter((s) => s.id !== id)
3046
3130
  await writeSources(sources)
3047
- sendJson(res, 200, { ok: true, sources })
3131
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3048
3132
  return
3049
3133
  }
3050
3134
  if (action === 'reset') {
3051
3135
  const defaults = JSON.parse(JSON.stringify(DEFAULT_SOURCES))
3052
3136
  await writeSources(defaults)
3053
- sendJson(res, 200, { ok: true, sources: defaults })
3137
+ sendJson(res, 200, { ok: true, sources: maskSources(defaults) })
3054
3138
  return
3055
3139
  }
3056
3140
  if (action === 'gitee-setup') {
3057
3141
  const clientId = typeof body.clientId === 'string' ? body.clientId.trim() : ''
3058
3142
  const clientSecret = typeof body.clientSecret === 'string' ? body.clientSecret.trim() : ''
3059
- if (!clientId || !clientSecret) {
3143
+ const keepClientId = body.keepClientId === true
3144
+ if ((!keepClientId && !clientId) || !clientSecret) {
3060
3145
  sendError(res, 400, 'client_id 与 client_secret 不能为空')
3061
3146
  return
3062
3147
  }
3063
3148
  const current = readGiteeConfig(sources)
3064
- sources.gitee = { ...current, clientId, clientSecret }
3149
+ // keepClientId:前端回显的是打码 clientId(abc12345…),用户只改了 secret 时保留原配置
3150
+ sources.gitee = { ...current, clientId: keepClientId ? current.clientId : clientId, clientSecret }
3065
3151
  await writeSources(sources)
3066
- sendJson(res, 200, { ok: true, sources })
3152
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3067
3153
  return
3068
3154
  }
3069
3155
  if (action === 'gitee-clear') {
3070
3156
  const current = readGiteeConfig(sources)
3071
3157
  sources.gitee = { clientId: current.clientId, clientSecret: current.clientSecret, token: '', login: '' }
3072
3158
  await writeSources(sources)
3073
- sendJson(res, 200, { ok: true, sources })
3159
+ sendJson(res, 200, { ok: true, sources: maskSources(sources) })
3074
3160
  return
3075
3161
  }
3076
3162
  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.9",
3
+ "version": "0.3.12",
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",