@noob-stupid/dsh-plugin-console 0.3.18 → 0.3.20
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/lib/client.js +53 -9
- package/lib/index.js +214 -14
- package/package.json +39 -39
package/lib/client.js
CHANGED
|
@@ -136,6 +136,7 @@ window.__ModuleLoader__.load({
|
|
|
136
136
|
loadingDetails: "正在读取详情…",
|
|
137
137
|
githubLoggedIn: "已登录 GitHub:",
|
|
138
138
|
githubCornerOut: "未登录 GitHub",
|
|
139
|
+
hubUpdateAvailable: "下载更新",
|
|
139
140
|
searchInstalledPlaceholder: "搜索已安装插件(名称或 id)",
|
|
140
141
|
extraFilter: "已装",
|
|
141
142
|
extraFilterAll: "全部",
|
|
@@ -370,6 +371,7 @@ window.__ModuleLoader__.load({
|
|
|
370
371
|
readmeLabel: "Readme summary",
|
|
371
372
|
loadingDetails: "Reading details…",
|
|
372
373
|
githubLoggedIn: "GitHub signed in: ",
|
|
374
|
+
hubUpdateAvailable: "Download update",
|
|
373
375
|
githubCornerOut: "Not signed in to GitHub",
|
|
374
376
|
searchInstalledPlaceholder: "Search installed plugins (name or id)",
|
|
375
377
|
extraFilter: "Installed",
|
|
@@ -763,7 +765,7 @@ window.__ModuleLoader__.load({
|
|
|
763
765
|
/** monorepo 子包列表:git trees 递归列出 packages 目录下的 package.json,逐个取 npm 包名(上限 24 个)。 */
|
|
764
766
|
async function fetchSubpackages(repo, branch) {
|
|
765
767
|
const data = await githubFetch(`https://api.github.com/repos/${encodeURIComponent(repo)}/git/trees/${encodeURIComponent(branch)}?recursive=1`);
|
|
766
|
-
const paths = (data.tree ?? []).filter((node) => node.type === "blob" && /^packages\/[^/]+\/package\.json$/u.test(node.path)).map((node) => node.path);
|
|
768
|
+
const paths = (data.tree ?? []).filter((node) => node.type === "blob" && /^(?:packages|examples|plugins|skills|apps|extensions|src|lib)\/[^/]+\/package\.json$/u.test(node.path)).map((node) => node.path);
|
|
767
769
|
if (paths.length === 0) return [];
|
|
768
770
|
const out = [];
|
|
769
771
|
for (const path of paths.slice(0, 24)) {
|
|
@@ -771,7 +773,7 @@ window.__ModuleLoader__.load({
|
|
|
771
773
|
if (text === null) continue;
|
|
772
774
|
try {
|
|
773
775
|
const pkg = JSON.parse(text);
|
|
774
|
-
if (pkg && typeof pkg.name === "string") out.push({ dir: path.split("/")[1], name: pkg.name });
|
|
776
|
+
if (pkg && typeof pkg.name === "string") out.push({ dir: path.split("/")[1], path: path.split("/").slice(0, -1).join("/"), name: pkg.name });
|
|
775
777
|
} catch {}
|
|
776
778
|
}
|
|
777
779
|
return out;
|
|
@@ -858,6 +860,8 @@ window.__ModuleLoader__.load({
|
|
|
858
860
|
const [frameworkUpgrading, setFrameworkUpgrading] = react.useState(false);
|
|
859
861
|
// 框架升级进度(重连后恢复显示):{status, message}
|
|
860
862
|
const [frameworkStatus, setFrameworkStatus] = react.useState(null);
|
|
863
|
+
// Hub 自身更新:仅在检测到远程新版时显示下载/更新按钮
|
|
864
|
+
const [selfUpdate, setSelfUpdate] = react.useState(null);
|
|
861
865
|
// 进度条步骤(与 host 脚本 SetState 的阶段对应)
|
|
862
866
|
const FW_STEPS = [
|
|
863
867
|
{ key: "starting", label: t("fwStepBackup") },
|
|
@@ -1066,6 +1070,22 @@ window.__ModuleLoader__.load({
|
|
|
1066
1070
|
};
|
|
1067
1071
|
run();
|
|
1068
1072
|
}, [market, state]);
|
|
1073
|
+
// Hub 自身更新检测:只在远程 npm 有更新时显示按钮
|
|
1074
|
+
react.useEffect(() => {
|
|
1075
|
+
if (state.status !== "ready") return;
|
|
1076
|
+
const current = state.data.selfVersion ?? null;
|
|
1077
|
+
if (!current) return;
|
|
1078
|
+
call("/plugin-console/check-update", { packageName: "@noob-stupid/dsh-plugin-console" }).then(
|
|
1079
|
+
(data) => {
|
|
1080
|
+
if (data && data.error === null && typeof data.latest === "string" && data.latest !== current) {
|
|
1081
|
+
setSelfUpdate({ latest: data.latest });
|
|
1082
|
+
} else {
|
|
1083
|
+
setSelfUpdate(null);
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
() => setSelfUpdate(null),
|
|
1087
|
+
);
|
|
1088
|
+
}, [state]);
|
|
1069
1089
|
// 搜索源菜单:点击外部收起
|
|
1070
1090
|
react.useEffect(() => {
|
|
1071
1091
|
if (!sourceMenuOpen) return;
|
|
@@ -1347,7 +1367,7 @@ window.__ModuleLoader__.load({
|
|
|
1347
1367
|
// 立即本地拼官方安装命令(不等服务端后台补拉),手动安装引导即时可见
|
|
1348
1368
|
if (!data.installCommand) {
|
|
1349
1369
|
const short = repo.split("/")[1] ?? repo;
|
|
1350
|
-
data.installCommand = "git
|
|
1370
|
+
data.installCommand = "git clone --recurse-submodules https://github.com/" + repo + ".git\ncd " + short + "\npowershell -ExecutionPolicy Bypass -File install.ps1";
|
|
1351
1371
|
}
|
|
1352
1372
|
}
|
|
1353
1373
|
setRepoInfo({ status: "ready", repo, data, direct: true });
|
|
@@ -1556,7 +1576,7 @@ window.__ModuleLoader__.load({
|
|
|
1556
1576
|
() => { setSearchSourceName(""); setSearchSourceUrl(""); setSearchSourceHeaders(""); },
|
|
1557
1577
|
);
|
|
1558
1578
|
};
|
|
1559
|
-
const startJob = (repo, packageName, source = "github", kind = "plugin") => {
|
|
1579
|
+
const startJob = (repo, packageName, source = "github", kind = "plugin", update = false) => {
|
|
1560
1580
|
// 防并发(与 addLocal 一致):已有安装/更新进行中时忽略新点击(更新按钮此前可绕过,
|
|
1561
1581
|
// 反复点击会产生几十个并发任务装同一插件)
|
|
1562
1582
|
if (installing !== null) {
|
|
@@ -1567,7 +1587,7 @@ window.__ModuleLoader__.load({
|
|
|
1567
1587
|
setReloadHint(false);
|
|
1568
1588
|
setRestartHint(false);
|
|
1569
1589
|
setMessage(null);
|
|
1570
|
-
call("/plugin-console/install", { repo: repo ?? "", packageName, source, kind }).then(
|
|
1590
|
+
call("/plugin-console/install", { repo: repo ?? "", packageName, source, kind, update }).then(
|
|
1571
1591
|
(data) => {
|
|
1572
1592
|
if (data && data.jobId) {
|
|
1573
1593
|
setJobs((prev) => ({
|
|
@@ -1629,6 +1649,11 @@ window.__ModuleLoader__.load({
|
|
|
1629
1649
|
startSuiteJob(item);
|
|
1630
1650
|
return;
|
|
1631
1651
|
}
|
|
1652
|
+
if (item.subpackagePath && typeof item.packageName === "string" && item.packageName !== "") {
|
|
1653
|
+
// monorepo 子包:搜索结果带 packageName,按 npm 包名安装
|
|
1654
|
+
startJob(item.fullName, item.packageName, item.source ?? "github");
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1632
1657
|
startJob(item.fullName, null, item.source ?? "github");
|
|
1633
1658
|
};
|
|
1634
1659
|
/** 停用/启用已安装技能(写入官方调用策略 frontmatter,可逆)。 */
|
|
@@ -1757,7 +1782,7 @@ window.__ModuleLoader__.load({
|
|
|
1757
1782
|
type: "button",
|
|
1758
1783
|
className: styles.toggle,
|
|
1759
1784
|
disabled: installing === entry.moduleName,
|
|
1760
|
-
onClick: () => startJob("", entry.moduleName),
|
|
1785
|
+
onClick: () => startJob("", entry.moduleName, "github", "plugin", true),
|
|
1761
1786
|
}, installing === entry.moduleName ? t("installing") : t("updateNow"))),
|
|
1762
1787
|
Array.isArray(updateCheck.depsOutdated) && updateCheck.depsOutdated.length > 0
|
|
1763
1788
|
? el("p", { className: styles.status, "data-error": "true" },
|
|
@@ -1970,7 +1995,7 @@ window.__ModuleLoader__.load({
|
|
|
1970
1995
|
type: "button",
|
|
1971
1996
|
className: styles.toggle,
|
|
1972
1997
|
disabled: installing === item.fullName,
|
|
1973
|
-
onClick: () => startJob("", installedMatch.moduleName),
|
|
1998
|
+
onClick: () => startJob("", installedMatch.moduleName, "github", "plugin", true),
|
|
1974
1999
|
}, installing === item.fullName ? t("installingLocal") : t("update") + " → v" + updateMap[item.fullName])
|
|
1975
2000
|
: el("button", {
|
|
1976
2001
|
type: "button",
|
|
@@ -2073,8 +2098,8 @@ window.__ModuleLoader__.load({
|
|
|
2073
2098
|
onClick: () => doToggleSkill(detailSkillName, detailSkillDisabled),
|
|
2074
2099
|
}, togglingSkill === detailSkillName ? t("installingLocal") : (detailSkillDisabled ? t("skillEnable") : t("skillDisable")))
|
|
2075
2100
|
: null,
|
|
2076
|
-
info.privateRoot ? el("div", null,
|
|
2077
|
-
el("p", { className: styles.status, "data-error": "true" }, t("privateRootHint")),
|
|
2101
|
+
(info.privateRoot || !info.hasPackageJson) ? el("div", null,
|
|
2102
|
+
info.privateRoot ? el("p", { className: styles.status, "data-error": "true" }, t("privateRootHint")) : null,
|
|
2078
2103
|
el("p", { className: styles.status }, t("subpackagesTitle") + ":"),
|
|
2079
2104
|
subpackages === null ? null
|
|
2080
2105
|
: subpackages.status === "loading"
|
|
@@ -2159,6 +2184,25 @@ window.__ModuleLoader__.load({
|
|
|
2159
2184
|
? t("marketTitle")
|
|
2160
2185
|
: t("marketTitleOther").replace("{source}", sourceDisplayName(searchSource)))),
|
|
2161
2186
|
el("div", { id: "pc-ghwrap", className: styles.ghwrap },
|
|
2187
|
+
selfUpdate !== null ? el("button", {
|
|
2188
|
+
type: "button",
|
|
2189
|
+
className: styles.ghpill,
|
|
2190
|
+
title: t("hubUpdateAvailable") + " v" + selfUpdate.latest,
|
|
2191
|
+
onClick: () => {
|
|
2192
|
+
setMessage(t("installing"));
|
|
2193
|
+
call("/plugin-console/self-update", {}).then(
|
|
2194
|
+
(data) => {
|
|
2195
|
+
if (data && data.ok === true) {
|
|
2196
|
+
setMessage((data.note ?? "已更新") + ",正在重启生效…");
|
|
2197
|
+
call("/plugin-console/restart", {}).then(() => {}, () => {});
|
|
2198
|
+
} else {
|
|
2199
|
+
setMessage(t("failed") + ":" + (data?.reason ?? "未知"));
|
|
2200
|
+
}
|
|
2201
|
+
},
|
|
2202
|
+
(error) => setMessage(t("failed") + ":" + friendlyGithubError(error).message),
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
}, "⬇ " + t("hubUpdateAvailable") + " v" + selfUpdate.latest) : null,
|
|
2162
2206
|
el("button", {
|
|
2163
2207
|
type: "button",
|
|
2164
2208
|
className: styles.ghpill,
|
package/lib/index.js
CHANGED
|
@@ -35,6 +35,26 @@ const GITHUB_UA = 'dsh-plugin-console/0.1 (local dsh web instance)'
|
|
|
35
35
|
/** Gitee OAuth 端点(第三方应用需在 gitee.com → 数据管理 → 第三方应用 创建)。 */
|
|
36
36
|
const GITEE_AUTH_URL = 'https://gitee.com/oauth/authorize'
|
|
37
37
|
const GITEE_TOKEN_URL = 'https://gitee.com/oauth/token'
|
|
38
|
+
/** Gitee OAuth state 一次性凭证(防止登录 CSRF / token 替换)。 */
|
|
39
|
+
const GITEE_OAUTH_STATES = new Map()
|
|
40
|
+
function createGiteeOAuthState() {
|
|
41
|
+
const state = globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
// 只保留最近 10 分钟内的 state
|
|
44
|
+
for (const [key, at] of GITEE_OAUTH_STATES) {
|
|
45
|
+
if (now - at > 10 * 60 * 1000) GITEE_OAUTH_STATES.delete(key)
|
|
46
|
+
}
|
|
47
|
+
GITEE_OAUTH_STATES.set(state, now)
|
|
48
|
+
return state
|
|
49
|
+
}
|
|
50
|
+
function consumeGiteeOAuthState(state) {
|
|
51
|
+
if (typeof state !== 'string' || state === '') return false
|
|
52
|
+
const at = GITEE_OAUTH_STATES.get(state)
|
|
53
|
+
if (at === undefined) return false
|
|
54
|
+
GITEE_OAUTH_STATES.delete(state)
|
|
55
|
+
return Date.now() - at <= 10 * 60 * 1000
|
|
56
|
+
}
|
|
57
|
+
|
|
38
58
|
const DEFAULT_SEARCH = 'dsh-plugin'
|
|
39
59
|
/** 技能市场收录的 topic(/search skills 分支三 topic 并行合并)。 */
|
|
40
60
|
const SKILL_TOPICS = ['agent-skills', 'claude-skills', 'dsh-skill']
|
|
@@ -1450,7 +1470,7 @@ async function fetchRepoPackage(repo, branch) {
|
|
|
1450
1470
|
* profile 目录由 pnpm 管理;若用 npm 写入会与 pnpm 的目录重建互相破坏
|
|
1451
1471
|
* (曾导致入口链接丢失、DSH 启动崩溃)。registry 走国内镜像。
|
|
1452
1472
|
*/
|
|
1453
|
-
async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmirror.com', timeout = 90000) {
|
|
1473
|
+
async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmirror.com', timeout = 90000, signal = null) {
|
|
1454
1474
|
const args = ['pnpm', 'add', spec, '--registry', registry]
|
|
1455
1475
|
const opts = {
|
|
1456
1476
|
cwd: profileDir,
|
|
@@ -1486,7 +1506,7 @@ async function pnpmInstall(profileDir, spec, registry = 'https://registry.npmmir
|
|
|
1486
1506
|
* 零依赖包可完整安装;带依赖包记录未补齐列表(不阻塞,供面板提示)。
|
|
1487
1507
|
* 返回 { version, missingDeps };失败抛错由调用方落入 next 通道。
|
|
1488
1508
|
*/
|
|
1489
|
-
async function curlManualInstall(profileDir, packageName, registries) {
|
|
1509
|
+
async function curlManualInstall(profileDir, packageName, registries, signal = null, exactVersion = null) {
|
|
1490
1510
|
let meta = null
|
|
1491
1511
|
let metaError = null
|
|
1492
1512
|
for (const reg of registries) {
|
|
@@ -1501,7 +1521,7 @@ async function curlManualInstall(profileDir, packageName, registries) {
|
|
|
1501
1521
|
}
|
|
1502
1522
|
}
|
|
1503
1523
|
if (!meta || typeof meta !== 'object') throw new Error(`curl 通道:无法获取 registry 元数据(${metaError?.message ?? '未知'})`)
|
|
1504
|
-
const version = meta['dist-tags']?.latest ?? null
|
|
1524
|
+
const version = exactVersion ?? meta['dist-tags']?.latest ?? null
|
|
1505
1525
|
const tarball = version ? meta.versions?.[version]?.dist?.tarball ?? null : null
|
|
1506
1526
|
if (!version || !tarball) throw new Error('curl 通道:registry 无 dist-tags.latest / tarball')
|
|
1507
1527
|
const bin = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
|
@@ -1509,9 +1529,16 @@ async function curlManualInstall(profileDir, packageName, registries) {
|
|
|
1509
1529
|
mkdirSync(tmp, { recursive: true })
|
|
1510
1530
|
try {
|
|
1511
1531
|
const tgz = join(tmp, 'pkg.tgz')
|
|
1512
|
-
await execFileAsync(bin, ['-s', '-L', '-m', '60', '-o', tgz, tarball], { timeout: 70000, windowsHide: true })
|
|
1513
|
-
await execFileAsync('tar', ['-xzf', tgz, '-C', tmp], { timeout: 30000, windowsHide: true })
|
|
1514
|
-
|
|
1532
|
+
await execFileAsync(bin, ['-s', '-L', '-m', '60', '-o', tgz, tarball], { timeout: 70000, windowsHide: true, ...(signal ? { signal } : {}) })
|
|
1533
|
+
await execFileAsync('tar', ['-xzf', tgz, '-C', tmp], { timeout: 30000, windowsHide: true, ...(signal ? { signal } : {}) })
|
|
1534
|
+
let pkgPath = join(tmp, 'package')
|
|
1535
|
+
if (!existsSync(join(pkgPath, 'package.json'))) {
|
|
1536
|
+
const candidates = readdirSync(tmp, { withFileTypes: true })
|
|
1537
|
+
.filter((d) => d.isDirectory())
|
|
1538
|
+
.map((d) => join(tmp, d.name))
|
|
1539
|
+
pkgPath = candidates.find((p) => existsSync(join(p, 'package.json'))) ?? pkgPath
|
|
1540
|
+
}
|
|
1541
|
+
if (!existsSync(join(pkgPath, 'package.json'))) throw new Error('curl 通道:解压后未找到含 package.json 的目录')
|
|
1515
1542
|
// 盒子实验:解压后先验证,通过才覆盖正式位置(失败保留旧版本,服务不中断)
|
|
1516
1543
|
const box = verifyPackageBox(pkgPath, profileDir, packageName)
|
|
1517
1544
|
const pkg = JSON.parse(readFileSync(join(pkgPath, 'package.json'), 'utf8'))
|
|
@@ -1532,6 +1559,34 @@ async function curlManualInstall(profileDir, packageName, registries) {
|
|
|
1532
1559
|
}
|
|
1533
1560
|
}
|
|
1534
1561
|
|
|
1562
|
+
/** 安装通道并行竞速:pnpm 与 curl 同时尝试,先成功者生效;失败方被 abort 不干扰。 */
|
|
1563
|
+
async function raceInstallChannels(profileDir, name, registries) {
|
|
1564
|
+
const controller = new AbortController()
|
|
1565
|
+
const signal = controller.signal
|
|
1566
|
+
const pnpmTask = (async () => {
|
|
1567
|
+
let lastError = null
|
|
1568
|
+
for (const registry of registries) {
|
|
1569
|
+
try {
|
|
1570
|
+
await pnpmInstall(profileDir, name, registry, 90000, signal)
|
|
1571
|
+
return { channel: 'pnpm', info: null }
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
lastError = error
|
|
1574
|
+
if (signal.aborted) throw error
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
throw lastError ?? new Error('pnpm 通道失败')
|
|
1578
|
+
})()
|
|
1579
|
+
const curlTask = (async () => {
|
|
1580
|
+
const info = await curlManualInstall(profileDir, name, registries, signal)
|
|
1581
|
+
return { channel: 'curl', info }
|
|
1582
|
+
})()
|
|
1583
|
+
const waitSuccess = (promise) => promise.then((value) => ({ value }), () => new Promise(() => {}))
|
|
1584
|
+
const timeout = new Promise((resolve) => setTimeout(() => resolve(null), 120000))
|
|
1585
|
+
const winner = await Promise.race([waitSuccess(pnpmTask), waitSuccess(curlTask), timeout])
|
|
1586
|
+
controller.abort()
|
|
1587
|
+
return winner ? winner.value : null
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1535
1590
|
/**
|
|
1536
1591
|
* 盒子实验验证:解压后的包目录先通过静态校验再允许覆盖正式位置。
|
|
1537
1592
|
* - package.json 必须可解析且 name 与安装目标一致
|
|
@@ -1570,7 +1625,7 @@ function verifyPackageBox(pkgPath, profileDir, packageName, refCheckRoot = null)
|
|
|
1570
1625
|
const refs = parseBundlePatchRefs(readFileSync(bundlePatch, 'utf8'))
|
|
1571
1626
|
const missingRefs = refs.filter((r) => !existsSync(join(checkRoot, r.name)))
|
|
1572
1627
|
if (missingRefs.length > 0) {
|
|
1573
|
-
|
|
1628
|
+
note.push(`bundle 引用缺失:${missingRefs.map((r) => r.name).join('、')}(安装后由聚合完整性检查补齐/禁用)`)
|
|
1574
1629
|
}
|
|
1575
1630
|
if (refs.length > 0) note.push(`bundle 引用 ${refs.length} 个已就位`)
|
|
1576
1631
|
}
|
|
@@ -2208,7 +2263,7 @@ async function fetchSubpackageNames(repo, branch, auth) {
|
|
|
2208
2263
|
try {
|
|
2209
2264
|
const data = await githubJson(`${GITHUB_API}/repos/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`, undefined, auth)
|
|
2210
2265
|
const paths = (data.tree ?? [])
|
|
2211
|
-
.filter((node) => node.type === 'blob' && /^packages\/[^/]+\/package\.json$/u.test(node.path))
|
|
2266
|
+
.filter((node) => node.type === 'blob' && /^(?:packages|examples|plugins|skills|apps|extensions|src|lib)\/[^/]+\/package\.json$/u.test(node.path))
|
|
2212
2267
|
.map((node) => node.path)
|
|
2213
2268
|
// 并行读取:黑洞期单条最坏 40s,24 条串行会拖到十几分钟
|
|
2214
2269
|
const results = await Promise.all(paths.slice(0, 24).map(async (path) => {
|
|
@@ -2216,7 +2271,7 @@ async function fetchSubpackageNames(repo, branch, auth) {
|
|
|
2216
2271
|
if (bodyText === null) return null
|
|
2217
2272
|
try {
|
|
2218
2273
|
const pkg = JSON.parse(bodyText)
|
|
2219
|
-
if (pkg && typeof pkg.name === 'string') return { dir: path.split('/')[1], name: pkg.name }
|
|
2274
|
+
if (pkg && typeof pkg.name === 'string') return { dir: path.split('/')[1], path: path.split('/').slice(0, -1).join('/'), name: pkg.name }
|
|
2220
2275
|
} catch {}
|
|
2221
2276
|
return null
|
|
2222
2277
|
}))
|
|
@@ -2358,6 +2413,36 @@ async function aiRepair(job, ctx, profileDir, candidates, lastError) {
|
|
|
2358
2413
|
}
|
|
2359
2414
|
}
|
|
2360
2415
|
|
|
2416
|
+
/** 聚合包更新后的子包版本对齐:按新版聚合包声明的依赖版本,逐个对齐已存在的子包。 */
|
|
2417
|
+
async function syncAggregateSubpackageVersions(profileDir, packageName, registries) {
|
|
2418
|
+
const pkgPath = join(profileDir, 'node_modules', packageName, 'package.json')
|
|
2419
|
+
if (!existsSync(pkgPath)) return []
|
|
2420
|
+
let pkg = null
|
|
2421
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { return [] }
|
|
2422
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.peerDependencies ?? {}) }
|
|
2423
|
+
const updated = []
|
|
2424
|
+
for (const dep of Object.keys(deps)) {
|
|
2425
|
+
if (dep.startsWith('@deepseek-ai/')) continue
|
|
2426
|
+
const spec = String(deps[dep] ?? '').replace(/^[\^~>=< ]+/u, '')
|
|
2427
|
+
if (!spec) continue
|
|
2428
|
+
const currentPath = join(profileDir, 'node_modules', dep, 'package.json')
|
|
2429
|
+
if (!existsSync(currentPath)) {
|
|
2430
|
+
try {
|
|
2431
|
+
await curlManualInstall(profileDir, dep, registries, null, spec)
|
|
2432
|
+
updated.push(`${dep}@${spec}(新装)`)
|
|
2433
|
+
} catch {}
|
|
2434
|
+
continue
|
|
2435
|
+
}
|
|
2436
|
+
try {
|
|
2437
|
+
const current = JSON.parse(readFileSync(currentPath, 'utf8'))
|
|
2438
|
+
if (current.version === spec) continue
|
|
2439
|
+
await curlManualInstall(profileDir, dep, registries, null, spec)
|
|
2440
|
+
updated.push(`${dep}@${spec}`)
|
|
2441
|
+
} catch {}
|
|
2442
|
+
}
|
|
2443
|
+
return updated
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2361
2446
|
async function runInstallJob(job, ctx) {
|
|
2362
2447
|
try {
|
|
2363
2448
|
job.stage = 'preparing'
|
|
@@ -2431,6 +2516,32 @@ async function runInstallJob(job, ctx) {
|
|
|
2431
2516
|
for (let index = 0; index < candidates.length && installedName === null; index += 1) {
|
|
2432
2517
|
if (Date.now() > deadline) break
|
|
2433
2518
|
const name = candidates[index]
|
|
2519
|
+
// 加法优化:包已在 node_modules 且名字匹配时,不再重复安装/触发 EPERM,直接进入启用流程
|
|
2520
|
+
if (installedName === null) {
|
|
2521
|
+
const existingTarget = join(profileDir, 'node_modules', name, 'package.json')
|
|
2522
|
+
if (existsSync(existingTarget)) {
|
|
2523
|
+
try {
|
|
2524
|
+
const existingPkg = JSON.parse(readFileSync(existingTarget, 'utf8'))
|
|
2525
|
+
if (existingPkg && existingPkg.name === name && job.update !== true) {
|
|
2526
|
+
installedName = name
|
|
2527
|
+
job.curlNote = `已检测到本地已安装 ${name}@${existingPkg.version ?? '?'},跳过重复下载`
|
|
2528
|
+
}
|
|
2529
|
+
} catch {}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
// 加法并行竞速:pnpm 与 curl 同时启动,先成功者生效;失败方 abort,不影响后续串行通道
|
|
2533
|
+
if (installedName === null && !subpackageMode && !expanded) {
|
|
2534
|
+
const raced = await raceInstallChannels(profileDir, name, registries)
|
|
2535
|
+
if (raced) {
|
|
2536
|
+
installedName = name
|
|
2537
|
+
if (raced.channel === 'curl') {
|
|
2538
|
+
const racedInfo = raced.info
|
|
2539
|
+
const stillMissing = await backfillMissingDeps(profileDir, racedInfo.missingDeps, registries)
|
|
2540
|
+
job.curlNote = `已通过并行 curl 通道安装 v${racedInfo.version}${stillMissing.length > 0 ? `(依赖仍未补齐:${stillMissing.join('、')},网络恢复后建议重新安装)` : '(捆绑依赖已补齐)'}`
|
|
2541
|
+
if (racedInfo.boxNote) job.curlNote += `(盒子验证:${racedInfo.boxNote})`
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2434
2545
|
// 通道 1..n:配置的软件源依次尝试(每源 90 秒封顶)
|
|
2435
2546
|
for (let ri = 0; ri < registries.length && installedName === null; ri += 1) {
|
|
2436
2547
|
try {
|
|
@@ -2561,6 +2672,13 @@ async function runInstallJob(job, ctx) {
|
|
|
2561
2672
|
return
|
|
2562
2673
|
}
|
|
2563
2674
|
job.packageName = installedName
|
|
2675
|
+
// 聚合包子包版本对齐(确保更新后所有子包也到新版声明版本)
|
|
2676
|
+
try {
|
|
2677
|
+
const synced = await syncAggregateSubpackageVersions(profileDir, installedName, registries)
|
|
2678
|
+
if (synced.length > 0) {
|
|
2679
|
+
job.bundleNote = (job.bundleNote ? job.bundleNote + ';' : '') + `子包版本对齐:更新 ${synced.length} 个(${synced.join('、')})`
|
|
2680
|
+
}
|
|
2681
|
+
} catch {}
|
|
2564
2682
|
// 聚合包完整性保障(防崩):bundle patch 引用的包缺失 → 补装;仍缺自动禁用该行
|
|
2565
2683
|
try {
|
|
2566
2684
|
const integrity = await ensureBundlePatchIntegrity(profileDir, installedName, patchPath)
|
|
@@ -2677,7 +2795,13 @@ async function handle(ctx, req, res) {
|
|
|
2677
2795
|
// 框架升级检测与适配(备份快照 + 重打框架补丁),try 包裹不阻塞 state 返回
|
|
2678
2796
|
let framework = null
|
|
2679
2797
|
try { framework = detectFrameworkUpgrade(ctx) } catch {}
|
|
2680
|
-
|
|
2798
|
+
let selfVersion = null
|
|
2799
|
+
try {
|
|
2800
|
+
const selfRequire = createRequire(import.meta.url)
|
|
2801
|
+
const selfPkg = JSON.parse(readFileSync(selfRequire.resolve('../package.json'), 'utf8'))
|
|
2802
|
+
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
2803
|
+
} catch {}
|
|
2804
|
+
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, selfVersion })
|
|
2681
2805
|
return
|
|
2682
2806
|
}
|
|
2683
2807
|
|
|
@@ -2697,13 +2821,15 @@ async function handle(ctx, req, res) {
|
|
|
2697
2821
|
const redirect = `http://127.0.0.1:${port}/plugin-console/gitee-oauth-callback`
|
|
2698
2822
|
// scope 请求 user_info + projects:Gitee 会校验请求的 scope 必须在应用已勾选的权限范围内,
|
|
2699
2823
|
// 应用权限必须同步勾选 user_info、projects,否则报「请求范围无效、未知或格式不正确」
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2824
|
+
const state = createGiteeOAuthState()
|
|
2825
|
+
const url = `${GITEE_AUTH_URL}?client_id=${encodeURIComponent(gitee.clientId)}&redirect_uri=${encodeURIComponent(redirect)}&response_type=code&scope=${encodeURIComponent('user_info projects')}&state=${encodeURIComponent(state)}`
|
|
2826
|
+
sendJson(res, 200, { ok: true, url, redirect, state })
|
|
2702
2827
|
return
|
|
2703
2828
|
}
|
|
2704
2829
|
|
|
2705
2830
|
if (method === 'GET' && pathname === `${ROUTE_PREFIX}/gitee-oauth-callback`) {
|
|
2706
2831
|
const code = new URL(req.url ?? '/', 'http://x').searchParams.get('code')
|
|
2832
|
+
const state = new URL(req.url ?? '/', 'http://x').searchParams.get('state')
|
|
2707
2833
|
const sources = readSources()
|
|
2708
2834
|
const gitee = readGiteeConfig(sources)
|
|
2709
2835
|
const failPage = (text) => {
|
|
@@ -2714,6 +2840,10 @@ async function handle(ctx, req, res) {
|
|
|
2714
2840
|
failPage('这是 Gitee 授权回调地址,不能直接访问。<br>正确流程:插件面板 → 软件源管理 → 填入 client_id / client_secret → 保存配置 → 点击「授权登录 Gitee」,授权完成后会自动跳回这里。')
|
|
2715
2841
|
return
|
|
2716
2842
|
}
|
|
2843
|
+
if (!state || !consumeGiteeOAuthState(state)) {
|
|
2844
|
+
failPage('Gitee OAuth state 校验失败,请重新发起授权。')
|
|
2845
|
+
return
|
|
2846
|
+
}
|
|
2717
2847
|
if (!gitee.clientId || !gitee.clientSecret) {
|
|
2718
2848
|
failPage('未配置 Gitee 应用:请先在 gitee.com 创建第三方应用(回调地址填本页完整地址),再在插件面板 → 软件源管理 中填入 client_id / client_secret 并保存。')
|
|
2719
2849
|
return
|
|
@@ -3035,6 +3165,48 @@ async function handle(ctx, req, res) {
|
|
|
3035
3165
|
)
|
|
3036
3166
|
items = normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github' }))
|
|
3037
3167
|
items = await enrichItems(items)
|
|
3168
|
+
// monorepo 子包增强(OpenViking/examples/dsh-memory-plugin 等可按子包名搜到)
|
|
3169
|
+
if (raw !== '' && !body.skills) {
|
|
3170
|
+
try {
|
|
3171
|
+
const codeData = await githubJson(
|
|
3172
|
+
`${GITHUB_API}/search/code?q=${encodeURIComponent(`${raw} filename:package.json`)}`,
|
|
3173
|
+
req.signal,
|
|
3174
|
+
auth.token,
|
|
3175
|
+
)
|
|
3176
|
+
const subItems = []
|
|
3177
|
+
for (const hit of (codeData.items ?? []).slice(0, 10)) {
|
|
3178
|
+
const hitPath = typeof hit.path === 'string' ? hit.path : ''
|
|
3179
|
+
if (!/^(?:packages|examples|plugins|skills|apps|extensions|src|lib)\/[^/]+\/package\.json$/u.test(hitPath)) continue
|
|
3180
|
+
const repoName = hit.repository?.full_name ?? ''
|
|
3181
|
+
if (!repoName) continue
|
|
3182
|
+
const dir = hitPath.split('/').slice(0, -1).join('/')
|
|
3183
|
+
let packageName = dir.split('/').slice(-1)[0]
|
|
3184
|
+
try {
|
|
3185
|
+
const pkgText = await rawTextWithFallback(repoName, 'main', hitPath)
|
|
3186
|
+
if (pkgText !== null) {
|
|
3187
|
+
const pkg = JSON.parse(pkgText)
|
|
3188
|
+
if (pkg && typeof pkg.name === 'string') packageName = pkg.name
|
|
3189
|
+
}
|
|
3190
|
+
} catch {}
|
|
3191
|
+
subItems.push({
|
|
3192
|
+
fullName: repoName,
|
|
3193
|
+
description: `子包:${dir}`,
|
|
3194
|
+
htmlUrl: `https://github.com/${repoName}/tree/main/${dir}`,
|
|
3195
|
+
stars: 0,
|
|
3196
|
+
updatedAt: '',
|
|
3197
|
+
defaultBranch: 'main',
|
|
3198
|
+
topics: [],
|
|
3199
|
+
source: 'github',
|
|
3200
|
+
subpackagePath: dir,
|
|
3201
|
+
packageName,
|
|
3202
|
+
})
|
|
3203
|
+
if (subItems.length >= 5) break
|
|
3204
|
+
}
|
|
3205
|
+
for (const sub of subItems) {
|
|
3206
|
+
if (!items.some((x) => x.fullName === sub.fullName)) items.push(sub)
|
|
3207
|
+
}
|
|
3208
|
+
} catch {}
|
|
3209
|
+
}
|
|
3038
3210
|
}
|
|
3039
3211
|
items = items.filter((item) => item.fullName !== '')
|
|
3040
3212
|
sendJson(res, 200, { ok: true, query, items, authenticated: auth.loggedIn, source })
|
|
@@ -3077,10 +3249,10 @@ async function handle(ctx, req, res) {
|
|
|
3077
3249
|
const short = repo.split('/')[1] ?? repo
|
|
3078
3250
|
const hasInstallScript = (await rawTextWithFallback(repo, branch, 'install.ps1')) !== null
|
|
3079
3251
|
|| (await rawTextWithFallback(repo, branch, 'install.sh')) !== null
|
|
3080
|
-
// 纯命令(无注释,CMD/PowerShell
|
|
3252
|
+
// 纯命令(无注释,CMD/PowerShell 通用);不再关闭 TLS 校验;
|
|
3081
3253
|
// 脚本用 powershell -File 调用,CMD 里也能跑
|
|
3082
3254
|
installCommand = [
|
|
3083
|
-
`git
|
|
3255
|
+
`git clone --recurse-submodules https://github.com/${repo}.git`,
|
|
3084
3256
|
`cd ${short}`,
|
|
3085
3257
|
hasInstallScript
|
|
3086
3258
|
? `powershell -ExecutionPolicy Bypass -File install.ps1`
|
|
@@ -3810,6 +3982,33 @@ async function handle(ctx, req, res) {
|
|
|
3810
3982
|
return
|
|
3811
3983
|
}
|
|
3812
3984
|
|
|
3985
|
+
if (pathname === `${ROUTE_PREFIX}/self-update`) {
|
|
3986
|
+
// Hub 自身一键更新:下载 npm 最新 tarball 到当前 profile,成功后由前端重启生效
|
|
3987
|
+
let selfVersion = null
|
|
3988
|
+
try {
|
|
3989
|
+
const selfPkg = JSON.parse(readFileSync(createRequire(import.meta.url).resolve('../package.json'), 'utf8'))
|
|
3990
|
+
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
3991
|
+
} catch {}
|
|
3992
|
+
let latest = null
|
|
3993
|
+
try {
|
|
3994
|
+
const data = await fetchJsonUrl('https://registry.npmmirror.com/@noob-stupid%2fdsh-plugin-console')
|
|
3995
|
+
latest = data?.['dist-tags']?.latest ?? null
|
|
3996
|
+
} catch {}
|
|
3997
|
+
if (!latest || selfVersion === null || latest === selfVersion) {
|
|
3998
|
+
sendJson(res, 200, { ok: false, current: selfVersion, latest, updated: false, reason: latest === selfVersion ? '已是最新版本' : '版本检测失败' })
|
|
3999
|
+
return
|
|
4000
|
+
}
|
|
4001
|
+
const profileDir = dirname(findPatchPath(ctx))
|
|
4002
|
+
const registries = orderedRegistries(readSources())
|
|
4003
|
+
try {
|
|
4004
|
+
const info = await curlManualInstall(profileDir, '@noob-stupid/dsh-plugin-console', registries)
|
|
4005
|
+
sendJson(res, 200, { ok: true, current: selfVersion, latest, updated: true, version: info.version, note: '已下载最新代码,重启服务后生效' })
|
|
4006
|
+
} catch (error) {
|
|
4007
|
+
sendError(res, 500, `Hub 自动更新失败:${error instanceof Error ? error.message : String(error)}`)
|
|
4008
|
+
}
|
|
4009
|
+
return
|
|
4010
|
+
}
|
|
4011
|
+
|
|
3813
4012
|
if (pathname === `${ROUTE_PREFIX}/install`) {
|
|
3814
4013
|
// repo 允许为空:纯 registry 更新(已安装插件的"检测更新"走此路径,无 git 兜底)
|
|
3815
4014
|
const rawRepo = typeof body.repo === 'string' ? body.repo.trim() : ''
|
|
@@ -3861,6 +4060,7 @@ async function handle(ctx, req, res) {
|
|
|
3861
4060
|
aiNote: null,
|
|
3862
4061
|
subpackages: null,
|
|
3863
4062
|
lastError: null,
|
|
4063
|
+
update: body.update === true,
|
|
3864
4064
|
kind,
|
|
3865
4065
|
}
|
|
3866
4066
|
installJobs.set(job.id, job)
|
package/package.json
CHANGED
|
@@ -1,39 +1,39 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "https://github.com/Noob-stupid/dsh-plugin-hub"
|
|
8
|
-
},
|
|
9
|
-
"type": "module",
|
|
10
|
-
"main": "./lib/index.js",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": "./lib/index.js",
|
|
13
|
-
"./client": "./lib/client.js",
|
|
14
|
-
"./package.json": "./package.json"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"dsh-plugin",
|
|
18
|
-
"deepseek-harness",
|
|
19
|
-
"plugin-manager"
|
|
20
|
-
],
|
|
21
|
-
"dsh": {
|
|
22
|
-
"bundle": {
|
|
23
|
-
"patch": "./cordis.patch.yml"
|
|
24
|
-
},
|
|
25
|
-
"client": {
|
|
26
|
-
"platform": "web",
|
|
27
|
-
"inject": [
|
|
28
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
29
|
-
"@deepseek-ai/dsh-client-locale",
|
|
30
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
31
|
-
]
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"files": [
|
|
35
|
-
"lib",
|
|
36
|
-
"cordis.patch.yml"
|
|
37
|
-
],
|
|
38
|
-
"license": "MIT"
|
|
39
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
+
"version": "0.3.20",
|
|
4
|
+
"description": "DSH plugin management panel & marketplace: one-click enable/disable, multi-source market (GitHub/Gitee/custom), static-index market (500+ plugins / 300 skills), skills, suites, and one-click framework upgrade.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/Noob-stupid/dsh-plugin-hub"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./lib/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./lib/index.js",
|
|
13
|
+
"./client": "./lib/client.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dsh-plugin",
|
|
18
|
+
"deepseek-harness",
|
|
19
|
+
"plugin-manager"
|
|
20
|
+
],
|
|
21
|
+
"dsh": {
|
|
22
|
+
"bundle": {
|
|
23
|
+
"patch": "./cordis.patch.yml"
|
|
24
|
+
},
|
|
25
|
+
"client": {
|
|
26
|
+
"platform": "web",
|
|
27
|
+
"inject": [
|
|
28
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
29
|
+
"@deepseek-ai/dsh-client-locale",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib",
|
|
36
|
+
"cordis.patch.yml"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT"
|
|
39
|
+
}
|