dsh-plugin-manager-plus 1.3.1 → 1.3.3
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 +136 -2
- package/lib/index.mjs +310 -4
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -74,7 +74,24 @@ window.__ModuleLoader__.load({
|
|
|
74
74
|
"func.ui": "界面增强",
|
|
75
75
|
"func.net": "网络接入",
|
|
76
76
|
"func.infra": "系统底层",
|
|
77
|
-
"func.other": "其他"
|
|
77
|
+
"func.other": "其他",
|
|
78
|
+
|
|
79
|
+
// 技术路线与版本兼容(v1.3.2)
|
|
80
|
+
"route.bundle": "自动激活",
|
|
81
|
+
"route.dual": "需登记",
|
|
82
|
+
"route.host": "纯宿主",
|
|
83
|
+
"route.local": "本地",
|
|
84
|
+
"route.unknown": "",
|
|
85
|
+
"route.hint.bundle": "自带 bundle 补丁:官方 CLI 安装后自动挂载激活行,无需手动登记",
|
|
86
|
+
"route.hint.dual": "无 bundle 补丁:需在补丁层登记激活行(插件管理器会自动登记)",
|
|
87
|
+
"route.hint.host": "纯宿主插件(无浏览器半)",
|
|
88
|
+
"route.hint.local": "本地 file:// 加载",
|
|
89
|
+
"verdict.ok": "✓ 适配",
|
|
90
|
+
"verdict.no": "⚠ 可能不兼容",
|
|
91
|
+
"verdict.unknown": "未声明版本",
|
|
92
|
+
"verdict.range": "声明 DSH:",
|
|
93
|
+
"tested.label": "实测",
|
|
94
|
+
"tested.title": "作者实测通过的 DSH 版本"
|
|
78
95
|
};
|
|
79
96
|
|
|
80
97
|
const en = {
|
|
@@ -132,7 +149,24 @@ window.__ModuleLoader__.load({
|
|
|
132
149
|
"func.ui": "UI & Display",
|
|
133
150
|
"func.net": "Network & Web",
|
|
134
151
|
"func.infra": "Infrastructure",
|
|
135
|
-
"func.other": "Other"
|
|
152
|
+
"func.other": "Other",
|
|
153
|
+
|
|
154
|
+
// route & verdict (v1.3.2)
|
|
155
|
+
"route.bundle": "auto-activate",
|
|
156
|
+
"route.dual": "needs row",
|
|
157
|
+
"route.host": "host-only",
|
|
158
|
+
"route.local": "local",
|
|
159
|
+
"route.unknown": "",
|
|
160
|
+
"route.hint.bundle": "Ships a bundle patch: the row mounts automatically on official CLI install",
|
|
161
|
+
"route.hint.dual": "No bundle patch: an activation row is required (the manager registers it)",
|
|
162
|
+
"route.hint.host": "Host-only plugin (no browser half)",
|
|
163
|
+
"route.hint.local": "Loaded via local file://",
|
|
164
|
+
"verdict.ok": "✓ compatible",
|
|
165
|
+
"verdict.no": "⚠ maybe incompatible",
|
|
166
|
+
"verdict.unknown": "no version declared",
|
|
167
|
+
"verdict.range": "Declared DSH:",
|
|
168
|
+
"tested.label": "tested",
|
|
169
|
+
"tested.title": "DSH versions the author tested"
|
|
136
170
|
};
|
|
137
171
|
|
|
138
172
|
/** 模块短名提取 */
|
|
@@ -208,6 +242,68 @@ window.__ModuleLoader__.load({
|
|
|
208
242
|
{ id: "disabled", labelKey: "status.disabled" }
|
|
209
243
|
];
|
|
210
244
|
|
|
245
|
+
/**
|
|
246
|
+
* 技术路线徽章配色(bundle=绿 / dual=橙 / host=蓝 / local=紫)
|
|
247
|
+
*/
|
|
248
|
+
function routeColor(route) {
|
|
249
|
+
switch (route) {
|
|
250
|
+
case "bundle": return { fg: "#1a7f37", bg: "rgba(26,127,55,0.12)" };
|
|
251
|
+
case "dual": return { fg: "#bc4c00", bg: "rgba(188,76,0,0.13)" };
|
|
252
|
+
case "host": return { fg: "#0a66c2", bg: "rgba(10,102,194,0.12)" };
|
|
253
|
+
case "local": return { fg: "#8250df", bg: "rgba(130,80,223,0.12)" };
|
|
254
|
+
default: return { fg: "var(--dsw-alias-label-tertiary)", bg: "var(--dsw-alias-bg-layer-1)" };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** 版本兼容徽章配色(ok=绿 / no=红 / unknown=灰) */
|
|
259
|
+
function verdictColor(verdict) {
|
|
260
|
+
switch (verdict) {
|
|
261
|
+
case "ok": return { fg: "#1a7f37", bg: "rgba(26,127,55,0.12)" };
|
|
262
|
+
case "no": return { fg: "#cf222e", bg: "rgba(207,34,46,0.13)" };
|
|
263
|
+
default: return { fg: "var(--dsw-alias-label-tertiary)", bg: "var(--dsw-alias-bg-layer-1)" };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** star 数格式化:>=1000 → 1.2k */
|
|
268
|
+
function formatStars(n) {
|
|
269
|
+
if (typeof n !== 'number' || !isFinite(n)) return "";
|
|
270
|
+
if (n >= 1000) {
|
|
271
|
+
const v = n / 1000;
|
|
272
|
+
return `⭐ ${(v >= 10 ? Math.round(v) : Math.round(v * 10) / 10)}k`;
|
|
273
|
+
}
|
|
274
|
+
return `⭐ ${n}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 路由+版本徽章小工具(市场搜索结果与已安装列表共用) */
|
|
278
|
+
function badgePair(t, pkg, options) {
|
|
279
|
+
return [
|
|
280
|
+
pkg.route && pkg.route !== "unknown" ? _jsx("span", {
|
|
281
|
+
title: t("route.hint." + pkg.route) || "",
|
|
282
|
+
style: {
|
|
283
|
+
fontSize: "11px",
|
|
284
|
+
padding: "1px 5px",
|
|
285
|
+
borderRadius: "4px",
|
|
286
|
+
color: routeColor(pkg.route).fg,
|
|
287
|
+
background: routeColor(pkg.route).bg,
|
|
288
|
+
border: "1px solid " + routeColor(pkg.route).bg
|
|
289
|
+
},
|
|
290
|
+
children: t("route." + pkg.route)
|
|
291
|
+
}, "r") : null,
|
|
292
|
+
pkg.verdict ? _jsx("span", {
|
|
293
|
+
title: pkg.dshRange ? `${t("verdict.range")} ${pkg.dshRange}` : t("verdict.noRange"),
|
|
294
|
+
style: {
|
|
295
|
+
fontSize: "11px",
|
|
296
|
+
padding: "1px 5px",
|
|
297
|
+
borderRadius: "4px",
|
|
298
|
+
color: verdictColor(pkg.verdict).fg,
|
|
299
|
+
background: verdictColor(pkg.verdict).bg,
|
|
300
|
+
border: "1px solid " + verdictColor(pkg.verdict).bg
|
|
301
|
+
},
|
|
302
|
+
children: t("verdict." + pkg.verdict)
|
|
303
|
+
}, "v") : null
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
|
|
211
307
|
/**
|
|
212
308
|
* =========================================================================
|
|
213
309
|
* 1. 插件管理标签页组件 (ManagerTab)
|
|
@@ -773,6 +869,25 @@ window.__ModuleLoader__.load({
|
|
|
773
869
|
]
|
|
774
870
|
}),
|
|
775
871
|
|
|
872
|
+
// 技术路线 + 版本兼容徽章(v1.3.2)
|
|
873
|
+
entry.route || entry.verdict ? _jsxs("div", {
|
|
874
|
+
style: { display: "flex", alignItems: "center", gap: "6px", flexWrap: "wrap" },
|
|
875
|
+
children: [
|
|
876
|
+
...badgePair(t, entry),
|
|
877
|
+
entry.tested && entry.tested.length ? _jsx("span", {
|
|
878
|
+
title: t("tested.title"),
|
|
879
|
+
style: {
|
|
880
|
+
fontSize: "11px",
|
|
881
|
+
color: "var(--dsw-alias-label-tertiary)",
|
|
882
|
+
background: "var(--dsw-alias-bg-layer-1)",
|
|
883
|
+
padding: "1px 5px",
|
|
884
|
+
borderRadius: "4px"
|
|
885
|
+
},
|
|
886
|
+
children: `${t("tested.label")} ${entry.tested.join(", ")}`
|
|
887
|
+
}) : null
|
|
888
|
+
]
|
|
889
|
+
}) : null,
|
|
890
|
+
|
|
776
891
|
// 标识与双分类标签
|
|
777
892
|
_jsxs("div", {
|
|
778
893
|
style: { display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "11px" },
|
|
@@ -1146,6 +1261,25 @@ window.__ModuleLoader__.load({
|
|
|
1146
1261
|
},
|
|
1147
1262
|
children: `v${pkg.version}`
|
|
1148
1263
|
}),
|
|
1264
|
+
...badgePair(t, pkg),
|
|
1265
|
+
// GitHub star(社区热度,点击打开仓库)
|
|
1266
|
+
typeof pkg.stars === "number" ? _jsx("a", {
|
|
1267
|
+
href: pkg.repoUrl || `https://github.com/${pkg.name}`,
|
|
1268
|
+
target: "_blank",
|
|
1269
|
+
rel: "noreferrer",
|
|
1270
|
+
title: pkg.repoUrl || "",
|
|
1271
|
+
style: {
|
|
1272
|
+
fontSize: "11px",
|
|
1273
|
+
padding: "1px 5px",
|
|
1274
|
+
borderRadius: "4px",
|
|
1275
|
+
color: "#e3b341",
|
|
1276
|
+
background: "rgba(227,179,65,0.12)",
|
|
1277
|
+
border: "1px solid rgba(227,179,65,0.25)",
|
|
1278
|
+
textDecoration: "none",
|
|
1279
|
+
cursor: "pointer"
|
|
1280
|
+
},
|
|
1281
|
+
children: formatStars(pkg.stars)
|
|
1282
|
+
}, "s") : null,
|
|
1149
1283
|
pkg.date ? _jsx("span", {
|
|
1150
1284
|
style: { fontSize: "11px", color: "var(--dsw-alias-label-tertiary)" },
|
|
1151
1285
|
children: pkg.date.slice(0, 10)
|
package/lib/index.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { homedir } from 'node:os';
|
|
|
15
15
|
export const name = 'dsh-plugin-manager-plus';
|
|
16
16
|
export const inject = ['loader', 'webServer'];
|
|
17
17
|
|
|
18
|
-
const PLUGIN_VERSION = '1.3.
|
|
18
|
+
const PLUGIN_VERSION = '1.3.3';
|
|
19
19
|
|
|
20
20
|
/** 读取宿主 dsh 包版本(后端进程 argv[1] 即 dsh 的 bin.js,其上级即包根) */
|
|
21
21
|
function readHostDshVersion() {
|
|
@@ -33,6 +33,256 @@ function readHostDshVersion() {
|
|
|
33
33
|
}
|
|
34
34
|
const HOST_DSH_VERSION = readHostDshVersion();
|
|
35
35
|
|
|
36
|
+
// =========================================================================
|
|
37
|
+
// 版本工具:社区插件的技术路线(route)与 DSH 版本兼容性(verdict)识别。
|
|
38
|
+
// 无第三方依赖,实现 npm semver 范围判断的实用子集(精确/^/~/>=/<=/>/</
|
|
39
|
+
// | |/x 通配/空格且),对无法解析的声明一律判 unknown 而非误报。
|
|
40
|
+
// =========================================================================
|
|
41
|
+
|
|
42
|
+
/** 解析 '1.2.3-alpha.1' → {major, minor, patch, pre};失败返回 null */
|
|
43
|
+
function parseSemver(v) {
|
|
44
|
+
const m = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?/.exec(String(v ?? '').trim());
|
|
45
|
+
if (!m) return null;
|
|
46
|
+
return { major: +m[1] || 0, minor: +m[2] || 0, patch: +m[3] || 0, pre: m[4] || '' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 比较两个已解析版本:a > b → 1,a < b → -1,相等 → 0 */
|
|
50
|
+
function compareSemver(a, b) {
|
|
51
|
+
if (!a || !b) return 0;
|
|
52
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
53
|
+
if (a[k] !== b[k]) return a[k] > b[k] ? 1 : -1;
|
|
54
|
+
}
|
|
55
|
+
if (a.pre === b.pre) return 0;
|
|
56
|
+
if (!a.pre) return 1; // 正式版 > pre-release
|
|
57
|
+
if (!b.pre) return -1;
|
|
58
|
+
const pa = a.pre.split(/[.-]/).map(s => (/^\d+$/.test(s) ? Number(s) : s));
|
|
59
|
+
const pb = b.pre.split(/[.-]/).map(s => (/^\d+$/.test(s) ? Number(s) : s));
|
|
60
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
61
|
+
const x = pa[i], y = pb[i];
|
|
62
|
+
if (x === undefined) return -1;
|
|
63
|
+
if (y === undefined) return 1;
|
|
64
|
+
if (x === y) continue;
|
|
65
|
+
if (typeof x === 'number' && typeof y === 'number') return x > y ? 1 : -1;
|
|
66
|
+
if (typeof x === 'number') return 1; // 数字段 < 字母段(npm 规则相反,此处从简)
|
|
67
|
+
if (typeof y === 'number') return -1;
|
|
68
|
+
return x > y ? 1 : -1;
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 单个比较器(>=、^、~、=、精确、x 通配)是否满足 */
|
|
74
|
+
function cmpSatisfies(v, cmpRaw) {
|
|
75
|
+
const vp = parseSemver(v);
|
|
76
|
+
if (!vp) return false;
|
|
77
|
+
let m = /^(>=|<=|>|<|\^|~|=)?\s*(.*)$/.exec(String(cmpRaw || '').trim());
|
|
78
|
+
const op = m[1] || '=';
|
|
79
|
+
let spec = (m[2] || '').trim();
|
|
80
|
+
if (!spec || spec === '*' || spec === 'x' || spec === 'X') return true;
|
|
81
|
+
// x 通配(如 1.x、1.2.x)
|
|
82
|
+
if (/^(\d+)(?:\.(\d+))?\.x$/i.test(spec) || /^(\d+)$/.test(spec) && op === '=' || /^(\d+)\.(\d+)$/.test(spec) && op === '=') {
|
|
83
|
+
const parts = spec.split('.');
|
|
84
|
+
const wantMaj = +parts[0];
|
|
85
|
+
if (vp.major !== wantMaj) return false;
|
|
86
|
+
if (parts.length >= 2 && parts[1] !== 'x' && parts[1] !== 'X' && vp.minor !== +parts[1]) return false;
|
|
87
|
+
if (parts.length >= 3 && parts[2] !== 'x' && parts[2] !== 'X' && vp.patch !== +parts[2]) return false;
|
|
88
|
+
if (op === '>') return false; // 对 x 通配不做精细上界,从宽
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// 处理组合范围如 ">=1.2.3 <2.0.0"(外层已按空格拆分)
|
|
92
|
+
const sp = parseSemver(spec);
|
|
93
|
+
if (!sp) return false;
|
|
94
|
+
switch (op) {
|
|
95
|
+
case '=': return compareSemver(vp, sp) === 0;
|
|
96
|
+
case '>': return compareSemver(vp, sp) > 0;
|
|
97
|
+
case '<': return compareSemver(vp, sp) < 0;
|
|
98
|
+
case '>=': return compareSemver(vp, sp) >= 0;
|
|
99
|
+
case '<=': return compareSemver(vp, sp) <= 0;
|
|
100
|
+
case '~': return vp.major === sp.major && vp.minor === sp.minor && compareSemver(vp, sp) >= 0;
|
|
101
|
+
case '^': {
|
|
102
|
+
const lower = compareSemver(vp, sp) >= 0;
|
|
103
|
+
if (!lower) return false;
|
|
104
|
+
if (sp.major > 0) return vp.major === sp.major;
|
|
105
|
+
if (sp.minor > 0) return vp.minor === sp.minor; // 0.x.y 系列
|
|
106
|
+
return vp.patch === sp.patch; // 0.0.x
|
|
107
|
+
}
|
|
108
|
+
default: return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 版本是否满足范围(支持 || 与空格分隔的多个比较器) */
|
|
113
|
+
function versionSatisfies(version, range) {
|
|
114
|
+
const r = String(range ?? '').trim();
|
|
115
|
+
if (!r || r === '*' || r === 'latest' || /^[a-zA-Z]/.test(r)) return true; // tag/无约束
|
|
116
|
+
const v = parseSemver(version);
|
|
117
|
+
if (!v) return false;
|
|
118
|
+
for (const or of r.split('||')) {
|
|
119
|
+
const parts = or.trim().split(/\s+/).filter(Boolean);
|
|
120
|
+
if (parts.length === 0) continue;
|
|
121
|
+
let ok = true;
|
|
122
|
+
for (const part of parts) {
|
|
123
|
+
if (part !== '' && !cmpSatisfies(version, part)) { ok = false; break; }
|
|
124
|
+
}
|
|
125
|
+
if (ok) return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 提取包的 DSH 版本约束(多个声明源,按优先级取首个非空)。
|
|
132
|
+
* 0 = dsh.compatibility.dsh(自定义声明,最明确)
|
|
133
|
+
* 1 = peerDependencies['@deepseek-ai/dsh']
|
|
134
|
+
* 2 = peerDependencies['@deepseek-ai/dsh-settings'](dshmarket 风格)
|
|
135
|
+
* 3 = peerDependencies['@deepseek-ai/cordis'](仅作参考,不判 verdict)
|
|
136
|
+
*/
|
|
137
|
+
function extractDshRange(pkg) {
|
|
138
|
+
if (!pkg || typeof pkg !== 'object') return null;
|
|
139
|
+
if (pkg.dsh?.compatibility?.dsh) return String(pkg.dsh.compatibility.dsh);
|
|
140
|
+
const peer = pkg.peerDependencies || {};
|
|
141
|
+
if (peer['@deepseek-ai/dsh']) return String(peer['@deepseek-ai/dsh']);
|
|
142
|
+
if (peer['@deepseek-ai/dsh-settings']) return String(peer['@deepseek-ai/dsh-settings']);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 判定社区插件安装技术路线:
|
|
148
|
+
* 'bundle' — 带 dsh.bundle.patch,官方 CLI add 后 bundle 层自动挂载激活行,
|
|
149
|
+
* 插件管理器安装时**不写**家级行(写则双源致命错误)
|
|
150
|
+
* 'dual' — 有 dsh.client(浏览器半)但无 bundle patch,需在 patch 层登记
|
|
151
|
+
* 激活行(裸包名或 file:// + client 行),插件管理器会登记
|
|
152
|
+
* 'host' — 纯宿主插件(无 dsh.client),仅需宿主激活行
|
|
153
|
+
* 'local' — file:// 本地加载(非 npm 包)
|
|
154
|
+
*/
|
|
155
|
+
function detectRoute(moduleName, pkg) {
|
|
156
|
+
if (typeof moduleName === 'string' && moduleName.startsWith('file://')) return 'local';
|
|
157
|
+
if (pkg?.dsh?.bundle?.patch) return 'bundle';
|
|
158
|
+
if (pkg?.dsh?.client?.platform) return 'dual';
|
|
159
|
+
if (pkg && typeof pkg === 'object') return 'host';
|
|
160
|
+
return 'unknown';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 组装某包的路线 + 版本兼容信息。
|
|
165
|
+
* @param {string} moduleName 加载 specifier(可能为 file:// URL)
|
|
166
|
+
* @param {object|null} pkg 已解析的 package.json(无则为 null)
|
|
167
|
+
* @returns {{route, dshRange, tested, enginesNode, verdict}}
|
|
168
|
+
*/
|
|
169
|
+
function assessPackage(moduleName, pkg) {
|
|
170
|
+
const route = detectRoute(moduleName, pkg);
|
|
171
|
+
const tested = pkg?.dsh?.compatibility?.tested?.length
|
|
172
|
+
? pkg.dsh.compatibility.tested.map(String)
|
|
173
|
+
: null;
|
|
174
|
+
const enginesNode = pkg?.engines?.node ? String(pkg.engines.node) : null;
|
|
175
|
+
const dshRange = extractDshRange(pkg);
|
|
176
|
+
let verdict = 'unknown';
|
|
177
|
+
if (dshRange && HOST_DSH_VERSION) {
|
|
178
|
+
verdict = versionSatisfies(HOST_DSH_VERSION, dshRange) ? 'ok' : 'no';
|
|
179
|
+
}
|
|
180
|
+
return { route, dshRange, tested, enginesNode, verdict };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 从已安装目录读取包 metadata(读不到返回 null);支持 file:// 本地插件向上找 package.json */
|
|
184
|
+
function readInstalledPkg(moduleName) {
|
|
185
|
+
try {
|
|
186
|
+
if (typeof moduleName === 'string' && moduleName.startsWith('file://')) {
|
|
187
|
+
let dir = dirname(fileURLToPath(moduleName.split('?')[0]));
|
|
188
|
+
for (let i = 0; i < 4; i++) {
|
|
189
|
+
const cand = join(dir, 'package.json');
|
|
190
|
+
if (existsSync(cand)) return JSON.parse(readFileSync(cand, 'utf8'));
|
|
191
|
+
const parent = dirname(dir);
|
|
192
|
+
if (parent === dir) break;
|
|
193
|
+
dir = parent;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (!moduleName || moduleName.startsWith('@deepseek-ai/')) return null; // 官方 bundle 行不读
|
|
198
|
+
const p = join(WEB_PROFILE_DIR, 'node_modules', ...String(moduleName).split('/'), 'package.json');
|
|
199
|
+
if (!existsSync(p)) return null;
|
|
200
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
201
|
+
} catch { return null; }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// =========================================================================
|
|
205
|
+
// GitHub star 数(社区热度)查询:metadata 的 repository 字段 → GitHub API。
|
|
206
|
+
// 内存缓存 30 分钟 + 并发池 6 + 失败静默降级(不阻塞搜索结果)。
|
|
207
|
+
// =========================================================================
|
|
208
|
+
|
|
209
|
+
const GITHUB_STAR_CACHE = new Map();
|
|
210
|
+
const STAR_TTL_MS = 30 * 60 * 1000;
|
|
211
|
+
|
|
212
|
+
/** 从 package.json 的 repository 字段提取 GitHub 仓库 'owner/name',非 GitHub 返回 null */
|
|
213
|
+
function extractGitHubRepo(pkg) {
|
|
214
|
+
if (!pkg || typeof pkg !== 'object') return null;
|
|
215
|
+
let raw = pkg.repository;
|
|
216
|
+
if (raw && typeof raw === 'object') raw = raw.url || raw.repository || '';
|
|
217
|
+
if (!raw || typeof raw !== 'string') {
|
|
218
|
+
// 部分包只写了 homepage (github 链接)
|
|
219
|
+
raw = typeof pkg.homepage === 'string' ? pkg.homepage : '';
|
|
220
|
+
}
|
|
221
|
+
raw = raw.trim();
|
|
222
|
+
if (!raw) return null;
|
|
223
|
+
let m = /^github:([^/\s]+\/[^/\s]+)/.exec(raw);
|
|
224
|
+
if (m) return m[1];
|
|
225
|
+
m = /^([\w.-]+\/[\w.-]+)$/.exec(raw); // npm 短格式 owner/repo
|
|
226
|
+
if (m) return m[1];
|
|
227
|
+
try {
|
|
228
|
+
const u = new URL(raw);
|
|
229
|
+
if (u.hostname === 'github.com') {
|
|
230
|
+
const parts = u.pathname.split('/').filter(Boolean);
|
|
231
|
+
if (parts.length >= 2) return parts[0] + '/' + parts[1].replace(/\.git$/i, '');
|
|
232
|
+
}
|
|
233
|
+
} catch {}
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 组装 GitHub 仓库主页 URL(供客户端跳转);非 GitHub 返回 null */
|
|
238
|
+
function gitHubRepoUrl(pkg) {
|
|
239
|
+
const repo = extractGitHubRepo(pkg);
|
|
240
|
+
if (!repo) return null;
|
|
241
|
+
return 'https://github.com/' + repo;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** 查询 GitHub star 数(带缓存;失败返回 null 且短暂缓存避免反复打 API) */
|
|
245
|
+
async function fetchGitHubStars(repo) {
|
|
246
|
+
const key = String(repo).toLowerCase();
|
|
247
|
+
const hit = GITHUB_STAR_CACHE.get(key);
|
|
248
|
+
if (hit && Date.now() - hit.at < STAR_TTL_MS) return hit.stars;
|
|
249
|
+
try {
|
|
250
|
+
const enc = String(repo).split('/').map(encodeURIComponent).join('/'); // 斜杠必须保留,%2F 会 404
|
|
251
|
+
const resp = await fetch(`https://api.github.com/repos/${enc}`, {
|
|
252
|
+
headers: {
|
|
253
|
+
Accept: 'application/vnd.github+json',
|
|
254
|
+
'User-Agent': 'dsh-plugin-manager-plus',
|
|
255
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
256
|
+
},
|
|
257
|
+
signal: AbortSignal.timeout(6000)
|
|
258
|
+
});
|
|
259
|
+
let stars = null;
|
|
260
|
+
if (resp.ok) {
|
|
261
|
+
const j = await resp.json();
|
|
262
|
+
if (typeof j.stargazers_count === 'number') stars = j.stargazers_count;
|
|
263
|
+
}
|
|
264
|
+
GITHUB_STAR_CACHE.set(key, { stars, at: Date.now() });
|
|
265
|
+
return stars;
|
|
266
|
+
} catch {
|
|
267
|
+
GITHUB_STAR_CACHE.set(key, { stars: null, at: Date.now() });
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 简单并发池:最多 limit 个并发执行 fn */
|
|
273
|
+
async function mapPool(items, limit, fn) {
|
|
274
|
+
const results = new Array(items.length);
|
|
275
|
+
let i = 0;
|
|
276
|
+
async function worker() {
|
|
277
|
+
while (i < items.length) {
|
|
278
|
+
const idx = i++;
|
|
279
|
+
try { results[idx] = await fn(items[idx], idx); } catch { results[idx] = undefined; }
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
|
|
36
286
|
/** Fiber 状态常量映射 */
|
|
37
287
|
const FIBER_PHASE = {
|
|
38
288
|
0: 'pending',
|
|
@@ -971,6 +1221,7 @@ export function apply(ctx) {
|
|
|
971
1221
|
const funcCat = determineFuncCategory(moduleName);
|
|
972
1222
|
// bundle 形态插件(行由包内 bundle 层挂载、经 manifest 跟踪)也可管理
|
|
973
1223
|
const manifestTracked = manifestEntryIds.has(cId || entryId);
|
|
1224
|
+
const assess = assessPackage(moduleName, readInstalledPkg(moduleName));
|
|
974
1225
|
|
|
975
1226
|
list.push({
|
|
976
1227
|
entryId: cId || entryId,
|
|
@@ -980,7 +1231,12 @@ export function apply(ctx) {
|
|
|
980
1231
|
source,
|
|
981
1232
|
manageable: isHomeInsert || manifestTracked,
|
|
982
1233
|
category: cat,
|
|
983
|
-
funcCategory: funcCat
|
|
1234
|
+
funcCategory: funcCat,
|
|
1235
|
+
route: assess.route,
|
|
1236
|
+
verdict: assess.verdict,
|
|
1237
|
+
dshRange: assess.dshRange,
|
|
1238
|
+
tested: assess.tested,
|
|
1239
|
+
enginesNode: assess.enginesNode
|
|
984
1240
|
});
|
|
985
1241
|
}
|
|
986
1242
|
}
|
|
@@ -990,6 +1246,7 @@ export function apply(ctx) {
|
|
|
990
1246
|
if (!seenCleanIds.has(id) && !seenCleanIds.has(cleanId(id))) {
|
|
991
1247
|
const cat = determineCategory('home', id, item.name, manifestEntryIds);
|
|
992
1248
|
const funcCat = determineFuncCategory(item.name);
|
|
1249
|
+
const assess = assessPackage(item.name, readInstalledPkg(item.name));
|
|
993
1250
|
|
|
994
1251
|
list.push({
|
|
995
1252
|
entryId: id,
|
|
@@ -999,7 +1256,12 @@ export function apply(ctx) {
|
|
|
999
1256
|
source: 'home',
|
|
1000
1257
|
manageable: true,
|
|
1001
1258
|
category: cat,
|
|
1002
|
-
funcCategory: funcCat
|
|
1259
|
+
funcCategory: funcCat,
|
|
1260
|
+
route: assess.route,
|
|
1261
|
+
verdict: assess.verdict,
|
|
1262
|
+
dshRange: assess.dshRange,
|
|
1263
|
+
tested: assess.tested,
|
|
1264
|
+
enginesNode: assess.enginesNode
|
|
1003
1265
|
});
|
|
1004
1266
|
}
|
|
1005
1267
|
}
|
|
@@ -1036,15 +1298,54 @@ export function apply(ctx) {
|
|
|
1036
1298
|
}
|
|
1037
1299
|
|
|
1038
1300
|
const rawResults = data?.objects || [];
|
|
1301
|
+
// 并发拉取候选包 metadata(npmmirror 快),用于判定技术路线与版本兼容;
|
|
1302
|
+
// 单个失败不影响整体(unknown 兜底)。
|
|
1303
|
+
const metaBy = new Map();
|
|
1304
|
+
const NAMES = rawResults.map(i => i.package?.name).filter(Boolean).slice(0, 25);
|
|
1305
|
+
await Promise.all(NAMES.map(async (n) => {
|
|
1306
|
+
try {
|
|
1307
|
+
const enc = n.split('/').map(encodeURIComponent).join('/');
|
|
1308
|
+
const resp = await fetch(`https://registry.npmmirror.com/${enc}`, {
|
|
1309
|
+
signal: AbortSignal.timeout(6000)
|
|
1310
|
+
});
|
|
1311
|
+
if (resp.ok) {
|
|
1312
|
+
const meta = await resp.json();
|
|
1313
|
+
const latest = meta['dist-tags']?.latest || meta.version;
|
|
1314
|
+
const ver = meta.versions?.[latest] || meta;
|
|
1315
|
+
metaBy.set(n, ver);
|
|
1316
|
+
}
|
|
1317
|
+
} catch {}
|
|
1318
|
+
}));
|
|
1319
|
+
|
|
1320
|
+
// GitHub star 数(社区热度):从 repo 字段提取 GitHub 仓库,
|
|
1321
|
+
// 走 GitHub API + 内存缓存 + 并发池;限流/无 repo/失败一律静默降级。
|
|
1322
|
+
const starBy = new Map();
|
|
1323
|
+
await mapPool(NAMES, 6, async (n) => {
|
|
1324
|
+
const meta = metaBy.get(n);
|
|
1325
|
+
const repo = meta ? extractGitHubRepo(meta) : null;
|
|
1326
|
+
if (!repo) return;
|
|
1327
|
+
const stars = await fetchGitHubStars(repo);
|
|
1328
|
+
if (typeof stars === 'number') starBy.set(n, stars);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1039
1331
|
const results = rawResults.map((item) => {
|
|
1040
1332
|
const pkg = item.package || {};
|
|
1041
1333
|
const isInstalled = installedNames.has(pkg.name);
|
|
1334
|
+
const meta = metaBy.get(pkg.name) || null;
|
|
1335
|
+
const assess = assessPackage(pkg.name, meta);
|
|
1042
1336
|
return {
|
|
1043
1337
|
name: pkg.name,
|
|
1044
1338
|
version: pkg.version,
|
|
1045
1339
|
description: pkg.description || '',
|
|
1046
1340
|
date: pkg.date || '',
|
|
1047
|
-
installed: isInstalled
|
|
1341
|
+
installed: isInstalled,
|
|
1342
|
+
route: assess.route,
|
|
1343
|
+
verdict: assess.verdict,
|
|
1344
|
+
dshRange: assess.dshRange,
|
|
1345
|
+
tested: assess.tested,
|
|
1346
|
+
enginesNode: assess.enginesNode,
|
|
1347
|
+
stars: starBy.get(pkg.name) ?? null,
|
|
1348
|
+
repoUrl: meta ? gitHubRepoUrl(meta) : null
|
|
1048
1349
|
};
|
|
1049
1350
|
});
|
|
1050
1351
|
|
|
@@ -1255,3 +1556,8 @@ export function apply(ctx) {
|
|
|
1255
1556
|
mountRoutes(ctx);
|
|
1256
1557
|
}
|
|
1257
1558
|
}
|
|
1559
|
+
|
|
1560
|
+
// 工具函数导出(供单测/外部复用;cordis 插件加载仅取 name/inject/apply,额外导出无害)
|
|
1561
|
+
export { versionSatisfies, extractDshRange, assessPackage, parseSemver, extractGitHubRepo, fetchGitHubStars, mapPool };
|
|
1562
|
+
|
|
1563
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-manager-plus",
|
|
3
3
|
"description": "DeepSeek Harness (DSH) 插件管理器:设置面板内的社区插件市场(npm 搜索一键安装)+ 已安装插件管理(来源/用途/状态三维筛选、启停热重载、一键卸载、持久化)。Plugin manager & marketplace for DeepSeek Harness.",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.mjs",
|
|
7
7
|
"exports": {
|
|
@@ -52,4 +52,5 @@
|
|
|
52
52
|
"url": "git+https://github.com/new-256/dsh-plugin-manager.git"
|
|
53
53
|
},
|
|
54
54
|
"license": "MIT"
|
|
55
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|