@noob-stupid/dsh-plugin-console 0.3.51 → 0.3.53
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 +151 -5
- package/lib/index.js +178 -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.53`**, 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.53`, 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.53` 等价**,只为可维护性;
|
|
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.53`,或本仓库 `main`。
|
|
282
282
|
- 请多反馈问题
|
package/lib/client.js
CHANGED
|
@@ -203,6 +203,15 @@ window.__ModuleLoader__.load({
|
|
|
203
203
|
loadingDetails: "正在读取详情…",
|
|
204
204
|
githubLoggedIn: "已登录 GitHub:",
|
|
205
205
|
githubCornerOut: "未登录 GitHub",
|
|
206
|
+
githubLoginViaApp: "GitHub 登录",
|
|
207
|
+
githubLoginOpened: "已打开 GitHub 授权窗口,请在页面上输入验证码完成授权…",
|
|
208
|
+
githubLoginUnavailable: "无法使用窗口登录",
|
|
209
|
+
githubLoginOrToken: "或用 Token 登录",
|
|
210
|
+
githubLoginTip: "粘贴 GitHub Token 登录(fine-grained,需 read:user 权限)。登录后可用代码搜索(按子包名搜索)并提高 API 限额;也可用 dsh-github-login 插件登录。",
|
|
211
|
+
githubTokenPlaceholder: "粘贴 token(ghp_… 或 github_pat_…)",
|
|
212
|
+
githubLoginBtn: "登录",
|
|
213
|
+
githubLoginOk: "已登录 GitHub:{login}",
|
|
214
|
+
githubLoginFail: "登录失败",
|
|
206
215
|
hubUpdateAvailable: "下载更新",
|
|
207
216
|
cleanResiduals: "清理残余备份",
|
|
208
217
|
searchInstalledPlaceholder: "搜索已安装插件(名称或 id)",
|
|
@@ -579,6 +588,15 @@ window.__ModuleLoader__.load({
|
|
|
579
588
|
hubUpdateAvailable: "Download update",
|
|
580
589
|
cleanResiduals: "Clean residual backups",
|
|
581
590
|
githubCornerOut: "Not signed in to GitHub",
|
|
591
|
+
githubLoginViaApp: "GitHub login",
|
|
592
|
+
githubLoginOpened: "Authorization window opened — enter the code on the GitHub page to finish",
|
|
593
|
+
githubLoginUnavailable: "Window login unavailable",
|
|
594
|
+
githubLoginOrToken: "Or sign in with a token",
|
|
595
|
+
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.",
|
|
596
|
+
githubTokenPlaceholder: "Paste token (ghp_… or github_pat_…)",
|
|
597
|
+
githubLoginBtn: "Sign in",
|
|
598
|
+
githubLoginOk: "Signed in to GitHub: {login}",
|
|
599
|
+
githubLoginFail: "Sign-in failed",
|
|
582
600
|
searchInstalledPlaceholder: "Search installed plugins (name or id)",
|
|
583
601
|
extraFilter: "Installed",
|
|
584
602
|
extraFilterAll: "All",
|
|
@@ -1703,6 +1721,10 @@ window.__ModuleLoader__.load({
|
|
|
1703
1721
|
try { const s = localStorage.getItem("pc-search-source"); return s === "gitee" ? s : "github"; } catch { return "github"; }
|
|
1704
1722
|
});
|
|
1705
1723
|
const [sourceMenuOpen, setSourceMenuOpen] = react.useState(false);
|
|
1724
|
+
// GitHub 登录:徽章点开内联面板,token 只在「浏览器 → 本机服务端」之间走,服务端不回显
|
|
1725
|
+
const [ghLoginOpen, setGhLoginOpen] = react.useState(false);
|
|
1726
|
+
const [ghToken, setGhToken] = react.useState("");
|
|
1727
|
+
const [ghLoginBusy, setGhLoginBusy] = react.useState(false);
|
|
1706
1728
|
// "不再提醒":勾选后 AI 兜底自动同意,不再弹窗(可在市场底部恢复提醒)
|
|
1707
1729
|
const [aiRemember, setAiRemember] = react.useState(() => {
|
|
1708
1730
|
try { return localStorage.getItem("pc-ai-remember") === "1"; } catch { return false; }
|
|
@@ -1919,6 +1941,89 @@ window.__ModuleLoader__.load({
|
|
|
1919
1941
|
);
|
|
1920
1942
|
}, []);
|
|
1921
1943
|
react.useEffect(() => { refresh(); }, [refresh]);
|
|
1944
|
+
// ── GitHub 登录态的两个读数:面板里多处要用(徽章文案、菜单项、内联子面板)──
|
|
1945
|
+
const ghAuthed = state.status === "ready" && state.data.github !== undefined && state.data.github.loggedIn === true;
|
|
1946
|
+
const ghLoginName = state.status === "ready" && state.data.github !== undefined && state.data.github.login !== null && state.data.github.login !== undefined
|
|
1947
|
+
? state.data.github.login
|
|
1948
|
+
: "unknown";
|
|
1949
|
+
// 设备码登录的轮询句柄:拿到登录态就停,用户手动收起菜单也停(不留后台空转的定时器)
|
|
1950
|
+
const ghPollRef = react.useRef(null);
|
|
1951
|
+
const stopGhPoll = react.useCallback(() => {
|
|
1952
|
+
if (ghPollRef.current !== null) { window.clearInterval(ghPollRef.current); ghPollRef.current = null; }
|
|
1953
|
+
}, []);
|
|
1954
|
+
// 组件卸载时收尾(本面板是长驻 UI,正常不卸载,但热重载/关闭面板会)
|
|
1955
|
+
react.useEffect(() => () => stopGhPoll(), [stopGhPoll]);
|
|
1956
|
+
// 菜单收起 = 用户手动关掉了这次登录尝试 → 立刻停止轮询(对应"最多 60s 或用户手动关闭")
|
|
1957
|
+
react.useEffect(() => { if (!sourceMenuOpen) stopGhPoll(); }, [sourceMenuOpen, stopGhPoll]);
|
|
1958
|
+
// 窗口登录开始后等登录态:每 2s 问一次 /state,最多 60s。
|
|
1959
|
+
// 为什么只能轮询:设备码流程在**另一个进程**里完成(用户在 GitHub 页面上输验证码授权),
|
|
1960
|
+
// 本插件收不到任何回调,只能盯 <DSH_HOME>/github-auth.json(/state 的 github 字段)有没有变。
|
|
1961
|
+
const startGhPoll = () => {
|
|
1962
|
+
stopGhPoll();
|
|
1963
|
+
const deadline = Date.now() + 60000;
|
|
1964
|
+
ghPollRef.current = window.setInterval(() => {
|
|
1965
|
+
call("/plugin-console/state").then(
|
|
1966
|
+
(snap) => {
|
|
1967
|
+
const github = snap !== null && snap !== undefined ? snap.github : null;
|
|
1968
|
+
if (github !== null && github !== undefined && github.loggedIn === true) {
|
|
1969
|
+
stopGhPoll();
|
|
1970
|
+
setMessage(t("githubLoginOk").replace("{login}", github.login ?? ""));
|
|
1971
|
+
refresh();
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
if (Date.now() >= deadline) stopGhPoll();
|
|
1975
|
+
},
|
|
1976
|
+
// 单次失败不中断:授权期间用户可能正在重启服务,下一拍再问
|
|
1977
|
+
() => { if (Date.now() >= deadline) stopGhPoll(); },
|
|
1978
|
+
);
|
|
1979
|
+
}, 2000);
|
|
1980
|
+
};
|
|
1981
|
+
// 菜单里的主入口「GitHub 登录」:首选设备码窗口流程(在 GitHub 页面上输验证码),
|
|
1982
|
+
// 通道不可用才降级到 token 粘贴子面板(token 是兜底,不是正路)。
|
|
1983
|
+
const githubOpenLogin = () => {
|
|
1984
|
+
if (ghLoginBusy) return;
|
|
1985
|
+
setGhLoginBusy(true);
|
|
1986
|
+
// 先给一句反馈:环回调用最长要等 8s,中间没有提示会让人以为点了没反应
|
|
1987
|
+
setMessage(t("githubLoginOpened"));
|
|
1988
|
+
call("/plugin-console/github-open-login", {}).then(
|
|
1989
|
+
(r) => {
|
|
1990
|
+
setGhLoginBusy(false);
|
|
1991
|
+
if (r !== null && r !== undefined && r.started === true) {
|
|
1992
|
+
setMessage(t("githubLoginOpened"));
|
|
1993
|
+
startGhPoll();
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
// 服务端回 started:false 时一定带 reason(插件没装/没有 exe/平台不支持),照念给用户
|
|
1997
|
+
setMessage(t("githubLoginUnavailable") + ":" + (r !== null && r !== undefined && typeof r.reason === "string" ? r.reason : ""));
|
|
1998
|
+
setGhLoginOpen(true);
|
|
1999
|
+
},
|
|
2000
|
+
(error) => {
|
|
2001
|
+
setGhLoginBusy(false);
|
|
2002
|
+
setMessage(t("githubLoginFail") + ":" + (error?.message ?? ""));
|
|
2003
|
+
setGhLoginOpen(true);
|
|
2004
|
+
},
|
|
2005
|
+
);
|
|
2006
|
+
};
|
|
2007
|
+
// 粘贴 token 登录:成功后清空输入 + 收起面板 + 刷新登录态(让徽章/代码搜索立刻用上新身份);
|
|
2008
|
+
// 失败**不清空输入**——用户多半是复制时多带了空格/少了一段,保留原文才好改。
|
|
2009
|
+
const githubLogin = () => {
|
|
2010
|
+
const token = ghToken.trim();
|
|
2011
|
+
if (token === "" || ghLoginBusy) return;
|
|
2012
|
+
setGhLoginBusy(true);
|
|
2013
|
+
call("/plugin-console/github-login", { token }).then(
|
|
2014
|
+
(r) => {
|
|
2015
|
+
setGhLoginBusy(false);
|
|
2016
|
+
setMessage(t("githubLoginOk").replace("{login}", r?.login ?? ""));
|
|
2017
|
+
setGhToken("");
|
|
2018
|
+
setGhLoginOpen(false);
|
|
2019
|
+
refresh();
|
|
2020
|
+
},
|
|
2021
|
+
(error) => {
|
|
2022
|
+
setGhLoginBusy(false);
|
|
2023
|
+
setMessage(t("githubLoginFail") + ":" + (error?.message ?? ""));
|
|
2024
|
+
},
|
|
2025
|
+
);
|
|
2026
|
+
};
|
|
1922
2027
|
const loadDetails = (entry) => {
|
|
1923
2028
|
if (details !== null && details.entryId === entry.entryId) {
|
|
1924
2029
|
setDetails(null);
|
|
@@ -3639,15 +3744,17 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
3639
3744
|
el("button", {
|
|
3640
3745
|
type: "button",
|
|
3641
3746
|
className: styles.ghpill,
|
|
3642
|
-
|
|
3747
|
+
// 外观回退:市场页只有**这一颗** pill,登录态体现在它的绿色描边上(data-on),
|
|
3748
|
+
// 不再单独占一颗登录徽章
|
|
3749
|
+
"data-on": ghAuthed ? "true" : "false",
|
|
3643
3750
|
title: t("searchSourceTitle"),
|
|
3644
3751
|
"aria-label": t("searchSourceTitle"),
|
|
3645
3752
|
onClick: () => setSourceMenuOpen((v) => !v),
|
|
3646
3753
|
},
|
|
3754
|
+
// 源选在 GitHub 时这颗 pill 顺手报登录态(回到上一批之前的形态);
|
|
3755
|
+
// 选别的源时只报源名 —— 否则"未登录 GitHub"看着像徽章却点开的是源菜单
|
|
3647
3756
|
searchSource === "github"
|
|
3648
|
-
? (
|
|
3649
|
-
? t("githubLoggedIn") + (state.data.github.login ?? "unknown")
|
|
3650
|
-
: t("githubCornerOut"))
|
|
3757
|
+
? (ghAuthed ? t("githubLoggedIn") + ghLoginName : t("githubCornerOut"))
|
|
3651
3758
|
: t("sourceOf") + ((searchSourcesList !== null ? searchSourcesList.find((s) => s.id === searchSource)?.name : undefined)
|
|
3652
3759
|
?? (searchSource === "gitee" ? "Gitee" : searchSource)),
|
|
3653
3760
|
" ▾"),
|
|
@@ -3667,7 +3774,46 @@ setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
|
3667
3774
|
try { localStorage.setItem("pc-search-source", s.id); } catch {}
|
|
3668
3775
|
setSourceMenuOpen(false);
|
|
3669
3776
|
},
|
|
3670
|
-
}, s.name))
|
|
3777
|
+
}, s.name)),
|
|
3778
|
+
// ── 登录区(登录入口收进这颗 pill 的菜单里,不再单独一颗徽章)──
|
|
3779
|
+
el("div", { style: { height: 1, margin: "4px 2px", background: "var(--dsw-alias-border-l2)" } }),
|
|
3780
|
+
ghAuthed
|
|
3781
|
+
// 已登录:这行只报状态(点了把状态念到消息栏,不做别的)
|
|
3782
|
+
? el("button", {
|
|
3783
|
+
type: "button",
|
|
3784
|
+
className: styles.sourceOpt,
|
|
3785
|
+
"data-active": "true",
|
|
3786
|
+
title: t("githubLoginTip"),
|
|
3787
|
+
onClick: () => setMessage(t("githubLoggedIn") + ghLoginName),
|
|
3788
|
+
}, t("githubLoggedIn") + ghLoginName)
|
|
3789
|
+
// 未登录:主入口走设备码窗口登录;窗口不可用时它自己会展开下面的 token 子面板
|
|
3790
|
+
: el("button", {
|
|
3791
|
+
type: "button",
|
|
3792
|
+
className: styles.sourceOpt,
|
|
3793
|
+
disabled: ghLoginBusy,
|
|
3794
|
+
title: t("githubLoginTip"),
|
|
3795
|
+
onClick: githubOpenLogin,
|
|
3796
|
+
}, t("githubLoginViaApp")),
|
|
3797
|
+
// 次要入口:token 兜底(已登录时也留着 —— 换账号/换 token 就靠它)
|
|
3798
|
+
el("button", {
|
|
3799
|
+
type: "button",
|
|
3800
|
+
className: styles.sourceOpt,
|
|
3801
|
+
onClick: () => setGhLoginOpen((v) => !v),
|
|
3802
|
+
}, t("githubLoginOrToken")),
|
|
3803
|
+
// token 兜底子面板:窗口登录不可用(插件没装/非 Windows/没有 exe)时自动展开;
|
|
3804
|
+
// 也可以从上面「或用 Token 登录」主动开合。样式内联,不为它改 CSS 表。
|
|
3805
|
+
ghLoginOpen
|
|
3806
|
+
? el("div", { style: { display: "flex", flexDirection: "column", gap: 6, width: 280, boxSizing: "border-box", padding: "2px 2px 4px" } },
|
|
3807
|
+
el("p", { className: styles.message, style: { margin: 0 } }, t("githubLoginTip")),
|
|
3808
|
+
el("input", {
|
|
3809
|
+
type: "password",
|
|
3810
|
+
placeholder: t("githubTokenPlaceholder"),
|
|
3811
|
+
value: ghToken,
|
|
3812
|
+
onChange: (event) => setGhToken(event.currentTarget.value),
|
|
3813
|
+
style: { width: "100%", boxSizing: "border-box" },
|
|
3814
|
+
}),
|
|
3815
|
+
el("button", { type: "button", className: styles.toggle, disabled: ghLoginBusy || ghToken.trim() === "", onClick: githubLogin }, t("githubLoginBtn")))
|
|
3816
|
+
: null)
|
|
3671
3817
|
: null,
|
|
3672
3818
|
)),
|
|
3673
3819
|
(aiRunning || aiJobsList.some((x) => x.status === "running"))
|
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,111 @@ 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
|
+
|
|
5558
|
+
if (pathname === `${ROUTE_PREFIX}/github-open-login`) {
|
|
5559
|
+
// 设备码(窗口)登录:唤起独立插件 dsh-github-login 的登录窗口 —— 登录的首选通道,token 只是兜底。
|
|
5560
|
+
//
|
|
5561
|
+
// 为什么是"调另一个插件的 HTTP 接口"而不是自己实现设备码:Device Flow + 桌面窗口 + 令牌落盘
|
|
5562
|
+
// 已经由那个插件实现好了,它挂在**同一个宿主 webServer** 上(源码:<profile>/node_modules/
|
|
5563
|
+
// dsh-github-login/lib/index.js):
|
|
5564
|
+
// POST /github-auth/open → { ok:true, launched:"<exe 路径>" } 或 { ok:true, launched:false, hint }
|
|
5565
|
+
// GET /github-auth/status → { ok, loggedIn, login }
|
|
5566
|
+
// 为什么失败也回 200:这个能力是**外挂**的 —— 插件可能没装、装了也可能没打包 exe、当前 profile
|
|
5567
|
+
// 也可能没把它挂上(2026-09-20 实测:本机装了它,但跑着的宿主里 GET /github-auth/status 仍是 404)。
|
|
5568
|
+
// 那是"这条通道现在不可用",不是"服务端出错":回 500 只会让用户看到一句看不懂的报错,还看不出
|
|
5569
|
+
// "该走 token 兜底"这条正路。所以统一回 200 + started:false + 可读 reason,由前端决定降级。
|
|
5570
|
+
const port = webPort(ctx)
|
|
5571
|
+
const base = `http://127.0.0.1:${port}`
|
|
5572
|
+
const unavailable = '未检测到 dsh-github-login 插件(或当前平台不支持它的登录窗口)'
|
|
5573
|
+
const fallback = (detail) => {
|
|
5574
|
+
sendJson(res, 200, { ok: true, started: false, reason: detail ? `${unavailable}:${detail}` : unavailable })
|
|
5575
|
+
}
|
|
5576
|
+
let opened = null
|
|
5577
|
+
try {
|
|
5578
|
+
const response = await fetch(`${base}/github-auth/open`, { method: 'POST', signal: AbortSignal.timeout(8000) })
|
|
5579
|
+
if (!response.ok) {
|
|
5580
|
+
fallback(`对方接口返回 HTTP ${response.status}`)
|
|
5581
|
+
return
|
|
5582
|
+
}
|
|
5583
|
+
opened = await response.json()
|
|
5584
|
+
} catch (error) {
|
|
5585
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
5586
|
+
// fetch 的 "fetch failed" 不带 cause 等于没说;超时原文也没有信息量 —— 都压成一行可读文案
|
|
5587
|
+
const cause = error?.cause?.code ?? error?.cause?.message ?? ''
|
|
5588
|
+
fallback(/abort/iu.test(message) ? '调用超时(8000ms)' : (cause ? `${message}(${cause})` : message))
|
|
5589
|
+
return
|
|
5590
|
+
}
|
|
5591
|
+
// 对方"接口在、但本机没有登录工具 exe"时回 { ok:true, launched:false, hint }:窗口其实没起来。
|
|
5592
|
+
// 这里必须跟着报 started:false —— 报 true 会让前端开 60s 轮询,等一个永远不会出现的登录。
|
|
5593
|
+
if (opened?.launched === false) {
|
|
5594
|
+
fallback(typeof opened.hint === 'string' && opened.hint !== '' ? opened.hint : '对方未启动登录窗口(未找到 DSH-GitHub-Login 可执行文件)')
|
|
5595
|
+
return
|
|
5596
|
+
}
|
|
5597
|
+
// 顺手把对方的登录态透传给前端(best-effort:拿不到就 status:null,不影响"窗口已打开"这个结论)
|
|
5598
|
+
let status = null
|
|
5599
|
+
try {
|
|
5600
|
+
const stateRes = await fetch(`${base}/github-auth/status`, { signal: AbortSignal.timeout(2000) })
|
|
5601
|
+
if (stateRes.ok) status = await stateRes.json()
|
|
5602
|
+
} catch {}
|
|
5603
|
+
sendJson(res, 200, { ok: true, started: true, status })
|
|
5604
|
+
return
|
|
5605
|
+
}
|
|
5606
|
+
|
|
5435
5607
|
if (pathname === `${ROUTE_PREFIX}/details`) {
|
|
5436
5608
|
const { entryId } = body
|
|
5437
5609
|
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.53",
|
|
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",
|