@noob-stupid/dsh-plugin-console 0.3.51 → 0.3.52
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 +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +77 -7
- package/lib/index.js +129 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -308,7 +308,7 @@ MIT
|
|
|
308
308
|
|
|
309
309
|
The layered-refactor preview lives in its **own repository**: [**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
|
|
310
310
|
|
|
311
|
-
- **What it is**: the monolithic `lib/index.js` (7637 lines) split into layers — `lib/index.js` **142 lines** + `lib/server/**` **36 modules**; **feature-equivalent to `0.3.
|
|
311
|
+
- **What it is**: the monolithic `lib/index.js` (7637 lines) split into layers — `lib/index.js` **142 lines** + `lib/server/**` **36 modules**; **feature-equivalent to `0.3.52`**, maintainability only;
|
|
312
312
|
- **Verified**: 18 test suites green · 8 architecture-guard assertions · **47/47 routes identical** (`status` + response fields) against the stable build;
|
|
313
313
|
- **Not yet exercised**: real framework upgrade / rollback, restart guardian, real install/uninstall, component start/stop, real AI run, Gitee OAuth (~10–25% long-tail risk, subjective);
|
|
314
|
-
- **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.
|
|
314
|
+
- **For everyday use** stick to npm `@noob-stupid/dsh-plugin-console@0.3.52`, or `main` of this repo.
|
package/README.zh.md
CHANGED
|
@@ -275,8 +275,8 @@ MIT
|
|
|
275
275
|
|
|
276
276
|
分层重构预览线在**独立仓库**:[**`Noob-stupid/dsh-plugin-hub-refactor`**](https://github.com/Noob-stupid/dsh-plugin-hub-refactor)
|
|
277
277
|
|
|
278
|
-
- **它是什么**:把单体 `lib/index.js`(7637 行)拆成分层结构 —— `lib/index.js` **142 行** + `lib/server/**` **36 个模块**,**功能与 `0.3.
|
|
278
|
+
- **它是什么**:把单体 `lib/index.js`(7637 行)拆成分层结构 —— `lib/index.js` **142 行** + `lib/server/**` **36 个模块**,**功能与 `0.3.52` 等价**,只为可维护性;
|
|
279
279
|
- **已验证**:18 套测试全绿 · 8 条架构守卫断言 · 与稳定版逐条对打 **47 条路由,`status` + 响应字段 47/47 一致**;
|
|
280
280
|
- **尚未实测**:真框架升级 / 真回滚、重启守护链路、真装真卸、组件进程启停、AI 真跑、Gitee OAuth 回调(长尾风险主观估计 **10%~25%**);
|
|
281
|
-
- **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.
|
|
281
|
+
- **日常使用请继续用**:npm `@noob-stupid/dsh-plugin-console@0.3.52`,或本仓库 `main`。
|
|
282
282
|
- 请多反馈问题
|
package/lib/client.js
CHANGED
|
@@ -203,6 +203,11 @@ window.__ModuleLoader__.load({
|
|
|
203
203
|
loadingDetails: "正在读取详情…",
|
|
204
204
|
githubLoggedIn: "已登录 GitHub:",
|
|
205
205
|
githubCornerOut: "未登录 GitHub",
|
|
206
|
+
githubLoginTip: "粘贴 GitHub Token 登录(fine-grained,需 read:user 权限)。登录后可用代码搜索(按子包名搜索)并提高 API 限额;也可用 dsh-github-login 插件登录。",
|
|
207
|
+
githubTokenPlaceholder: "粘贴 token(ghp_… 或 github_pat_…)",
|
|
208
|
+
githubLoginBtn: "登录",
|
|
209
|
+
githubLoginOk: "已登录 GitHub:{login}",
|
|
210
|
+
githubLoginFail: "登录失败",
|
|
206
211
|
hubUpdateAvailable: "下载更新",
|
|
207
212
|
cleanResiduals: "清理残余备份",
|
|
208
213
|
searchInstalledPlaceholder: "搜索已安装插件(名称或 id)",
|
|
@@ -579,6 +584,11 @@ window.__ModuleLoader__.load({
|
|
|
579
584
|
hubUpdateAvailable: "Download update",
|
|
580
585
|
cleanResiduals: "Clean residual backups",
|
|
581
586
|
githubCornerOut: "Not signed in to GitHub",
|
|
587
|
+
githubLoginTip: "Paste a GitHub token to sign in (fine-grained, needs the read:user scope). Signing in enables code search (search by subpackage name) and raises API rate limits; you can also sign in with the dsh-github-login plugin.",
|
|
588
|
+
githubTokenPlaceholder: "Paste token (ghp_… or github_pat_…)",
|
|
589
|
+
githubLoginBtn: "Sign in",
|
|
590
|
+
githubLoginOk: "Signed in to GitHub: {login}",
|
|
591
|
+
githubLoginFail: "Sign-in failed",
|
|
582
592
|
searchInstalledPlaceholder: "Search installed plugins (name or id)",
|
|
583
593
|
extraFilter: "Installed",
|
|
584
594
|
extraFilterAll: "All",
|
|
@@ -1703,6 +1713,10 @@ window.__ModuleLoader__.load({
|
|
|
1703
1713
|
try { const s = localStorage.getItem("pc-search-source"); return s === "gitee" ? s : "github"; } catch { return "github"; }
|
|
1704
1714
|
});
|
|
1705
1715
|
const [sourceMenuOpen, setSourceMenuOpen] = react.useState(false);
|
|
1716
|
+
// GitHub 登录:徽章点开内联面板,token 只在「浏览器 → 本机服务端」之间走,服务端不回显
|
|
1717
|
+
const [ghLoginOpen, setGhLoginOpen] = react.useState(false);
|
|
1718
|
+
const [ghToken, setGhToken] = react.useState("");
|
|
1719
|
+
const [ghLoginBusy, setGhLoginBusy] = react.useState(false);
|
|
1706
1720
|
// "不再提醒":勾选后 AI 兜底自动同意,不再弹窗(可在市场底部恢复提醒)
|
|
1707
1721
|
const [aiRemember, setAiRemember] = react.useState(() => {
|
|
1708
1722
|
try { return localStorage.getItem("pc-ai-remember") === "1"; } catch { return false; }
|
|
@@ -1892,6 +1906,16 @@ window.__ModuleLoader__.load({
|
|
|
1892
1906
|
document.addEventListener("mousedown", onDown);
|
|
1893
1907
|
return () => document.removeEventListener("mousedown", onDown);
|
|
1894
1908
|
}, [sourceMenuOpen]);
|
|
1909
|
+
// GitHub 登录面板:点击外部收起(与搜索源菜单同一模式)
|
|
1910
|
+
react.useEffect(() => {
|
|
1911
|
+
if (!ghLoginOpen) return;
|
|
1912
|
+
const onDown = (event) => {
|
|
1913
|
+
const area = document.getElementById("pc-ghlogin");
|
|
1914
|
+
if (area && !area.contains(event.target)) setGhLoginOpen(false);
|
|
1915
|
+
};
|
|
1916
|
+
document.addEventListener("mousedown", onDown);
|
|
1917
|
+
return () => document.removeEventListener("mousedown", onDown);
|
|
1918
|
+
}, [ghLoginOpen]);
|
|
1895
1919
|
// 已安装搜索:点击搜索区以外且无内容时立即收起(无延迟)
|
|
1896
1920
|
react.useEffect(() => {
|
|
1897
1921
|
if (!installedSearchOpen) return;
|
|
@@ -1919,6 +1943,26 @@ window.__ModuleLoader__.load({
|
|
|
1919
1943
|
);
|
|
1920
1944
|
}, []);
|
|
1921
1945
|
react.useEffect(() => { refresh(); }, [refresh]);
|
|
1946
|
+
// 粘贴 token 登录:成功后清空输入 + 收起面板 + 刷新登录态(让徽章/代码搜索立刻用上新身份);
|
|
1947
|
+
// 失败**不清空输入**——用户多半是复制时多带了空格/少了一段,保留原文才好改。
|
|
1948
|
+
const githubLogin = () => {
|
|
1949
|
+
const token = ghToken.trim();
|
|
1950
|
+
if (token === "" || ghLoginBusy) return;
|
|
1951
|
+
setGhLoginBusy(true);
|
|
1952
|
+
call("/plugin-console/github-login", { token }).then(
|
|
1953
|
+
(r) => {
|
|
1954
|
+
setGhLoginBusy(false);
|
|
1955
|
+
setMessage(t("githubLoginOk").replace("{login}", r?.login ?? ""));
|
|
1956
|
+
setGhToken("");
|
|
1957
|
+
setGhLoginOpen(false);
|
|
1958
|
+
refresh();
|
|
1959
|
+
},
|
|
1960
|
+
(error) => {
|
|
1961
|
+
setGhLoginBusy(false);
|
|
1962
|
+
setMessage(t("githubLoginFail") + ":" + (error?.message ?? ""));
|
|
1963
|
+
},
|
|
1964
|
+
);
|
|
1965
|
+
};
|
|
1922
1966
|
const loadDetails = (entry) => {
|
|
1923
1967
|
if (details !== null && details.entryId === entry.entryId) {
|
|
1924
1968
|
setDetails(null);
|
|
@@ -3639,17 +3683,14 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
3639
3683
|
el("button", {
|
|
3640
3684
|
type: "button",
|
|
3641
3685
|
className: styles.ghpill,
|
|
3642
|
-
"data-on": state.status === "ready" && state.data.github && state.data.github.loggedIn ? "true" : "false",
|
|
3643
3686
|
title: t("searchSourceTitle"),
|
|
3644
3687
|
"aria-label": t("searchSourceTitle"),
|
|
3645
3688
|
onClick: () => setSourceMenuOpen((v) => !v),
|
|
3646
3689
|
},
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
: t("sourceOf") + ((searchSourcesList !== null ? searchSourcesList.find((s) => s.id === searchSource)?.name : undefined)
|
|
3652
|
-
?? (searchSource === "gitee" ? "Gitee" : searchSource)),
|
|
3690
|
+
// 这一颗是「搜索源」选择器:只报当前源名。登录态搬去右边的 GitHub 徽章
|
|
3691
|
+
// (原来它顺手显示登录态,导致"未登录 GitHub"看着像徽章却点开的是源菜单)
|
|
3692
|
+
t("sourceOf") + ((searchSourcesList !== null ? searchSourcesList.find((s) => s.id === searchSource)?.name : undefined)
|
|
3693
|
+
?? (searchSource === "gitee" ? "Gitee" : searchSource)),
|
|
3653
3694
|
" ▾"),
|
|
3654
3695
|
sourceMenuOpen
|
|
3655
3696
|
? el("div", { className: styles.sourceMenu },
|
|
@@ -3669,6 +3710,35 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
3669
3710
|
},
|
|
3670
3711
|
}, s.name)))
|
|
3671
3712
|
: null,
|
|
3713
|
+
// 「未登录 GitHub / 已登录 GitHub:xxx」徽章:原来只是展示(点了没反应),
|
|
3714
|
+
// 现在点开内联登录面板(粘贴 PAT 即登录),面板样式内联,避免为它改 CSS 表。
|
|
3715
|
+
el("div", { id: "pc-ghlogin", style: { position: "relative", display: "inline-flex" } },
|
|
3716
|
+
el("button", {
|
|
3717
|
+
type: "button",
|
|
3718
|
+
className: styles.ghpill,
|
|
3719
|
+
"data-on": state.status === "ready" && state.data.github && state.data.github.loggedIn ? "true" : "false",
|
|
3720
|
+
title: t("githubLoginTip"),
|
|
3721
|
+
"aria-label": t("githubLoginTip"),
|
|
3722
|
+
onClick: () => setGhLoginOpen((v) => !v),
|
|
3723
|
+
},
|
|
3724
|
+
state.status === "ready" && state.data.github && state.data.github.loggedIn
|
|
3725
|
+
? t("githubLoggedIn") + (state.data.github.login ?? "unknown")
|
|
3726
|
+
: t("githubCornerOut")),
|
|
3727
|
+
ghLoginOpen
|
|
3728
|
+
? el("div", { style: { position: "absolute", right: 0, top: "calc(100% + 6px)", zIndex: 95, width: 300, boxSizing: "border-box", border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", borderRadius: 8, padding: 10, display: "flex", flexDirection: "column", gap: 8, textAlign: "left", boxShadow: "0 8px 24px rgba(0,0,0,.18)" } },
|
|
3729
|
+
el("p", { className: styles.message, style: { margin: 0 } }, t("githubLoginTip")),
|
|
3730
|
+
state.status === "ready" && state.data.github && state.data.github.loggedIn
|
|
3731
|
+
? el("p", { className: styles.message, style: { margin: 0 } }, t("githubLoggedIn") + (state.data.github.login ?? "unknown"))
|
|
3732
|
+
: null,
|
|
3733
|
+
el("input", {
|
|
3734
|
+
type: "password",
|
|
3735
|
+
placeholder: t("githubTokenPlaceholder"),
|
|
3736
|
+
value: ghToken,
|
|
3737
|
+
onChange: (event) => setGhToken(event.currentTarget.value),
|
|
3738
|
+
style: { width: "100%", boxSizing: "border-box" },
|
|
3739
|
+
}),
|
|
3740
|
+
el("button", { type: "button", className: styles.toggle, disabled: ghLoginBusy || ghToken.trim() === "", onClick: githubLogin }, t("githubLoginBtn")))
|
|
3741
|
+
: null),
|
|
3672
3742
|
)),
|
|
3673
3743
|
(aiRunning || aiJobsList.some((x) => x.status === "running"))
|
|
3674
3744
|
? el("button", {
|
package/lib/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 移除该块即恢复;HMR 监视器会自动重组合,无需重启。
|
|
14
14
|
*/
|
|
15
15
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
16
|
-
import { readFileSync, writeFileSync, existsSync, statSync, rmSync, readdirSync, mkdirSync, copyFileSync, realpathSync } from 'node:fs'
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, statSync, rmSync, readdirSync, mkdirSync, copyFileSync, realpathSync, chmodSync } from 'node:fs'
|
|
17
17
|
import { execFile, spawn } from 'node:child_process'
|
|
18
18
|
import { promisify } from 'node:util'
|
|
19
19
|
import { dirname, join, basename, sep } from 'node:path'
|
|
@@ -1800,14 +1800,19 @@ const GH_BIN_CANDIDATES = [
|
|
|
1800
1800
|
join(homedir(), 'scoop', 'shims', 'gh.exe'),
|
|
1801
1801
|
]
|
|
1802
1802
|
|
|
1803
|
-
|
|
1803
|
+
/** gh CLI 通道。传 token 时用 GH_TOKEN/GITHUB_TOKEN 环境变量**覆盖 gh 的 keyring 凭据** ——
|
|
1804
|
+
* 这是"校验某个 token 是否有效"时唯一安全的用法:否则 gh 会拿本机已登录的身份作答,
|
|
1805
|
+
* 一个填错的 token 也会"验证成功"(详见 /github-login 路由里的说明)。 */
|
|
1806
|
+
function githubViaGh(apiPath, token = null) {
|
|
1807
|
+
// 环境变量而不是命令行参数:token 不能出现在进程命令行(同机其他进程可见)
|
|
1808
|
+
const env = token === null ? undefined : { ...process.env, GH_TOKEN: token, GITHUB_TOKEN: token }
|
|
1804
1809
|
return new Promise((resolve, reject) => {
|
|
1805
1810
|
const attempt = (index) => {
|
|
1806
1811
|
if (index >= GH_BIN_CANDIDATES.length) {
|
|
1807
1812
|
reject(new Error('gh api 兜底失败:未找到 gh CLI'))
|
|
1808
1813
|
return
|
|
1809
1814
|
}
|
|
1810
|
-
execFile(GH_BIN_CANDIDATES[index], ['api', apiPath, '--jq', '.'], { windowsHide: true, timeout: 45000, maxBuffer: 8 * 1024 * 1024 }, (error, stdout) => {
|
|
1815
|
+
execFile(GH_BIN_CANDIDATES[index], ['api', apiPath, '--jq', '.'], { windowsHide: true, timeout: 45000, maxBuffer: 8 * 1024 * 1024, ...(env === undefined ? {} : { env }) }, (error, stdout) => {
|
|
1811
1816
|
if (error) {
|
|
1812
1817
|
// ENOENT = 该路径不存在,继续下一个候选;其他错误(网络/认证)直接失败
|
|
1813
1818
|
if (error.code === 'ENOENT') {
|
|
@@ -1866,6 +1871,52 @@ async function githubText(url, signal, token = null) {
|
|
|
1866
1871
|
return body
|
|
1867
1872
|
}
|
|
1868
1873
|
|
|
1874
|
+
/**
|
|
1875
|
+
* 用**给定 token 专属**的通道取当前用户(GitHub 登录名校验用)。
|
|
1876
|
+
*
|
|
1877
|
+
* 为什么不能直接用 githubJson:它内部会与 gh CLI 通道并行竞速(raceFirst2xx),而 gh 默认用
|
|
1878
|
+
* 本机 keyring 里已有的凭据、完全忽略我们传入的 token —— 本机装着已登录的 gh 时,一个填错的
|
|
1879
|
+
* token 也会因 gh 通道 2xx 而"验证成功"(2026-09-20 实测:本机 gh 登录 Noob-stupid,ghp_aaa…
|
|
1880
|
+
* 拿到的是 Noob-stupid),于是错误 token 被写进 github-auth.json,顶掉用户真实登录态。
|
|
1881
|
+
*
|
|
1882
|
+
* 两条通道都只认用户给的这个 token(gh 走 GH_TOKEN 环境变量覆盖 keyring 凭据):
|
|
1883
|
+
* · https(官方 + 镜像):通用主通道,无子进程
|
|
1884
|
+
* · gh:node:https 被中间设备劫持时(本机实测 "unable to verify the first certificate")
|
|
1885
|
+
* 的唯一可用通道 —— 少了它,本机贴对 token 也会被判成"令牌无效"
|
|
1886
|
+
* 两条通道认证的是同一个 token,所以谁先成功都等价;一旦某条给出 401/403(令牌本身不对),
|
|
1887
|
+
* 立刻采信,不等另一条慢超时。
|
|
1888
|
+
*/
|
|
1889
|
+
async function githubJsonUserWithToken(token) {
|
|
1890
|
+
const viaHttps = (async () => {
|
|
1891
|
+
const res = await githubRequest(`${GITHUB_API}/user`, { token })
|
|
1892
|
+
const status = res.statusCode ?? 0
|
|
1893
|
+
const body = await collectBody(res)
|
|
1894
|
+
// 与 githubJson 同一套错误语义:限流单独说明,其余只报 HTTP 状态(都不含 token)
|
|
1895
|
+
if (status === 403 && Number(res.headers['x-ratelimit-remaining'] ?? '1') === 0) {
|
|
1896
|
+
throw new Error('GitHub 接口限流已用尽,请稍后再试')
|
|
1897
|
+
}
|
|
1898
|
+
if (status < 200 || status >= 300) throw new Error(`GitHub 请求失败 (HTTP ${status})`)
|
|
1899
|
+
return JSON.parse(body)
|
|
1900
|
+
})()
|
|
1901
|
+
const attempts = [viaHttps, githubViaGh('/user', token)]
|
|
1902
|
+
return await new Promise((resolve, reject) => {
|
|
1903
|
+
let pending = attempts.length
|
|
1904
|
+
let firstError = null
|
|
1905
|
+
for (const attempt of attempts) {
|
|
1906
|
+
attempt.then(resolve, (error) => {
|
|
1907
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1908
|
+
if (/HTTP (?:401|403)/u.test(message)) {
|
|
1909
|
+
reject(error)
|
|
1910
|
+
return
|
|
1911
|
+
}
|
|
1912
|
+
if (firstError === null) firstError = error
|
|
1913
|
+
pending -= 1
|
|
1914
|
+
if (pending === 0) reject(firstError)
|
|
1915
|
+
})
|
|
1916
|
+
}
|
|
1917
|
+
})
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1869
1920
|
/** 用 curl 子进程请求 JSON(绕过本机中间设备对 node:https TLS 指纹的拦截;curl 走系统网络栈)。 */
|
|
1870
1921
|
async function curlJson(url, timeout = 15000, headers = {}, { ipv4 = false } = {}) {
|
|
1871
1922
|
const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
|
@@ -4820,12 +4871,28 @@ async function runInstallJob(job, ctx) {
|
|
|
4820
4871
|
if (pkg.private === true) {
|
|
4821
4872
|
// 私有 monorepo 根:自动列出子包作为候选(把人工修复经验自动化),聚合包优先
|
|
4822
4873
|
subpackageMode = true
|
|
4823
|
-
|
|
4824
|
-
|
|
4874
|
+
// 2026-09-20 事故(用户点装 zhu1090093659/dsh-web,报"未发现子包"):该仓库根包确实
|
|
4875
|
+
// private: true,但 main/dev 各有 22 个子包目录。失败原因是当时市场索引源全挂、网络受限,
|
|
4876
|
+
// subpackageCandidates() 读不到列表 —— 旧代码只有"有没有子包"一个出口,把**没读到**
|
|
4877
|
+
// 报成了**不存在**,直接把用户带偏。教训:探测失败必须与确定性结论分开表达。
|
|
4878
|
+
let subs = await subpackageCandidates(job.repo, branch)
|
|
4879
|
+
if (subs.length === 0) {
|
|
4880
|
+
// 读不到时先换一条分支重试:meta 探测失败时 branch 恒为 main,而默认分支为 dev 的仓库
|
|
4881
|
+
// (本例 dsh-web 就是 dev 为默认分支)main 上的子包布局可能不同/为空;
|
|
4882
|
+
// 换分支几乎零成本,却能把"分支取错"这一类假失败挡在报错之前。
|
|
4883
|
+
const altBranch = branch === 'main' ? 'dev' : 'main'
|
|
4884
|
+
subs = await subpackageCandidates(job.repo, altBranch)
|
|
4885
|
+
if (subs.length > 0) job.subpackageNote = `子包列表取自 ${altBranch} 分支(默认分支探测可能失败)`
|
|
4886
|
+
}
|
|
4887
|
+
if (subs.length === 0) {
|
|
4825
4888
|
job.status = 'failed'
|
|
4826
|
-
|
|
4889
|
+
// 文案要点:说清"这是读不到、不是没有",并给出可直接复制的安装命令 ——
|
|
4890
|
+
// 用户看到"未发现子包"会去翻仓库找,而真相多半只是本次网络没读成。
|
|
4891
|
+
job.error = `仓库 ${job.repo} 的根包未发布到 npm(private: true),且本次没能读到它的子包列表(多为网络受限/超时,不代表没有子包)。请重试;或在「查看」详情里确认子包名后直接安装,例如:dsh plugin --profile web add @linxin666/dsh-web-all`
|
|
4892
|
+
job.probeReason = 'subpackages-unreadable'
|
|
4827
4893
|
return
|
|
4828
4894
|
}
|
|
4895
|
+
candidates = subs
|
|
4829
4896
|
job.subpackages = candidates
|
|
4830
4897
|
} else {
|
|
4831
4898
|
candidates = [pkg.name]
|
|
@@ -5432,6 +5499,62 @@ async function handle(ctx, req, res) {
|
|
|
5432
5499
|
|
|
5433
5500
|
const body = await readBody(req)
|
|
5434
5501
|
|
|
5502
|
+
if (pathname === `${ROUTE_PREFIX}/github-login`) {
|
|
5503
|
+
// GitHub 登录(市场页「未登录 GitHub」徽章 → 内联面板):用粘贴的 token 换登录名并写入
|
|
5504
|
+
// <DSH_HOME>/github-auth.json —— 与独立插件 dsh-github-login 同一份文件、同一格式 { token, login }。
|
|
5505
|
+
//
|
|
5506
|
+
// 三条安全约束(改这里时不要破):
|
|
5507
|
+
// ① token 只进不出:响应只回 login,绝不回显 token,也不写任何日志(日志会落进 console 文件)
|
|
5508
|
+
// ② 先校验形状再发网络请求:明显不是 PAT 的串不浪费一次 GitHub 往返
|
|
5509
|
+
// ③ 落盘尽量收紧权限(POSIX 0600;Windows 无此权限模型,失败忽略)
|
|
5510
|
+
const token = typeof body?.token === 'string' ? body.token.trim() : ''
|
|
5511
|
+
// classic(ghp_/gho_/ghu_/ghs_/ghr_)与 fine-grained(github_pat_)两种 PAT 形状
|
|
5512
|
+
if (!/^(?:gh[pousr]_[A-Za-z0-9]{20,255}|github_pat_[A-Za-z0-9_]{20,255})$/u.test(token)) {
|
|
5513
|
+
sendError(res, 400, '令牌格式不正确(应为 GitHub personal access token,ghp_/github_pat_ 开头)')
|
|
5514
|
+
return
|
|
5515
|
+
}
|
|
5516
|
+
// 为什么不用 githubJson 校验:它内部会与 gh CLI 通道并行竞速(raceFirst2xx),而 gh 默认用
|
|
5517
|
+
// 本机 keyring 里已有的凭据、**完全忽略我们传入的 token**。本机装了已登录的 gh 时,一个填错的
|
|
5518
|
+
// token 也会因 gh 通道 2xx 而"验证成功"(2026-09-20 实测:本机 gh 登录 Noob-stupid,ghp_aaa…
|
|
5519
|
+
// 能拿到 Noob-stupid),于是错误 token 被写进 github-auth.json,把用户真实登录态顶掉。
|
|
5520
|
+
// githubJsonUserWithToken 的两条通道都只认用户给的这个 token(详见其注释)。
|
|
5521
|
+
let login = null
|
|
5522
|
+
try {
|
|
5523
|
+
const me = await githubJsonUserWithToken(token)
|
|
5524
|
+
login = typeof me?.login === 'string' && me.login !== '' ? me.login : null
|
|
5525
|
+
} catch (error) {
|
|
5526
|
+
// gh 通道的原始报错带 execFile 的 "Command failed: gh api …" 命令回显与多行 stderr,
|
|
5527
|
+
// 压成一行再给用户看(原文只含状态与命令名,不含 token —— 我们从不把 token 传进命令行)
|
|
5528
|
+
const message = (error instanceof Error ? error.message : String(error))
|
|
5529
|
+
.replace(/Command failed:[^\n]*\n?/gu, '')
|
|
5530
|
+
.replace(/\s+/gu, ' ')
|
|
5531
|
+
.trim()
|
|
5532
|
+
// 与"子包列表读不到 ≠ 不存在"同一课:**"没验成"不等于"令牌无效"**。通道全挂(证书/超时/DNS)
|
|
5533
|
+
// 时报"令牌无效"会把用户往错的方向带(去重新生成 token),必须分开说;文案里仍然不带 token 本身。
|
|
5534
|
+
const transport = /证书|certificate|超时|timeout|ECONN|ENOTFOUND|EAI_AGAIN|getaddrinfo|socket|network|aborted|未找到 gh CLI/iu.test(message)
|
|
5535
|
+
sendError(res, 400, transport
|
|
5536
|
+
? `无法连接 GitHub 校验令牌(网络/证书问题,不代表令牌无效,可重试):${message}`
|
|
5537
|
+
: `令牌无效或权限不足:${message}`)
|
|
5538
|
+
return
|
|
5539
|
+
}
|
|
5540
|
+
if (login === null) {
|
|
5541
|
+
sendError(res, 400, '令牌无效或权限不足:GitHub 未返回登录名')
|
|
5542
|
+
return
|
|
5543
|
+
}
|
|
5544
|
+
const authFile = join(dshHome(), 'github-auth.json')
|
|
5545
|
+
try {
|
|
5546
|
+
mkdirSync(dshHome(), { recursive: true })
|
|
5547
|
+
// mode 只在新建时生效,所以再显式 chmod 一次:老文件可能是 0644(token 不该对同机其他用户可读)
|
|
5548
|
+
await writeFile(authFile, `${JSON.stringify({ token, login }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
|
|
5549
|
+
try { chmodSync(authFile, 0o600) } catch {}
|
|
5550
|
+
} catch (error) {
|
|
5551
|
+
sendError(res, 500, `写入登录状态失败:${error instanceof Error ? error.message : String(error)}`)
|
|
5552
|
+
return
|
|
5553
|
+
}
|
|
5554
|
+
sendJson(res, 200, { ok: true, login })
|
|
5555
|
+
return
|
|
5556
|
+
}
|
|
5557
|
+
|
|
5435
5558
|
if (pathname === `${ROUTE_PREFIX}/details`) {
|
|
5436
5559
|
const { entryId } = body
|
|
5437
5560
|
if (typeof entryId !== 'string' || !/^[A-Za-z0-9_:.-]{1,80}$/u.test(entryId)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.52",
|
|
4
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
5
|
"repository": {
|
|
6
6
|
"type": "git",
|