@1e0zj/dsh-plugin-mall 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/client.js +34 -11
- package/src/github.js +113 -17
- package/src/index.js +21 -4
- package/src/installer.js +19 -3
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -43,10 +43,13 @@ window.__ModuleLoader__.load({
|
|
|
43
43
|
".mkt_cardActions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:auto;padding-top:4px}",
|
|
44
44
|
".mkt_cardActions .mkt_btn{text-decoration:none}",
|
|
45
45
|
".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
|
|
46
|
+
".mkt_panelRow{display:flex;align-items:center;gap:8px}",
|
|
47
|
+
".mkt_panelRow .mkt_link{margin-left:auto}",
|
|
46
48
|
".mkt_pre{font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-secondary,#f6f7f8);border-radius:6px;padding:8px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all}",
|
|
47
49
|
".mkt_ok{color:var(--dsw-alias-state-success-primary,#2f855a)}",
|
|
48
50
|
".mkt_badge{display:inline-block;border-radius:999px;padding:1px 8px;font-size:11px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}",
|
|
49
51
|
".mkt_badgeOk{border-color:var(--dsw-alias-state-success-primary,#2f855a);color:var(--dsw-alias-state-success-primary,#2f855a)}",
|
|
52
|
+
".mkt_badgeBad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
|
|
50
53
|
".mkt_check{display:flex;align-items:center;gap:4px;font-size:12.5px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap}",
|
|
51
54
|
".mkt_link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;cursor:pointer}",
|
|
52
55
|
".mkt_installedHead{display:flex;align-items:center;gap:8px;width:100%;background:none;border:0;padding:0;margin:0;cursor:pointer;font:inherit;text-align:left}",
|
|
@@ -127,7 +130,11 @@ window.__ModuleLoader__.load({
|
|
|
127
130
|
jobsRef.current = Object.assign({}, jobsRef.current, { [id]: { status: "running", spec: spec, output: "" } });
|
|
128
131
|
setJobs(Object.assign({}, jobsRef.current));
|
|
129
132
|
}, []);
|
|
130
|
-
|
|
133
|
+
var clear = useCallback(function () {
|
|
134
|
+
jobsRef.current = {};
|
|
135
|
+
setJobs({});
|
|
136
|
+
}, []);
|
|
137
|
+
return { jobs: jobs, track: track, clear: clear };
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
// ── plugin verification badge ───────────────────────────────────────────
|
|
@@ -135,6 +142,9 @@ window.__ModuleLoader__.load({
|
|
|
135
142
|
// dsh.bundle/dsh.client 声明,进程内缓存)。unknown 不显示徽章。
|
|
136
143
|
function verifyBadge(verified) {
|
|
137
144
|
if (verified === undefined || verified === null) return null;
|
|
145
|
+
if (verified.hostDeps !== undefined && verified.hostDeps.length > 0) {
|
|
146
|
+
return h("span", { className: "mkt_badge mkt_badgeBad", title: verified.hostDeps.join(", ") }, "宿主依赖风险");
|
|
147
|
+
}
|
|
138
148
|
if (verified.kind === "bundle") return h("span", { className: "mkt_badge mkt_badgeOk" }, "宿主插件");
|
|
139
149
|
if (verified.kind === "client") return h("span", { className: "mkt_badge mkt_badgeOk" }, "UI插件");
|
|
140
150
|
if (verified.kind === "plain") return h("span", { className: "mkt_badge" }, "未声明");
|
|
@@ -181,7 +191,10 @@ window.__ModuleLoader__.load({
|
|
|
181
191
|
var ids = Object.keys(props.jobs);
|
|
182
192
|
if (ids.length === 0) return null;
|
|
183
193
|
return h("div", { className: "mkt_card" },
|
|
184
|
-
h("
|
|
194
|
+
h("div", { className: "mkt_panelRow" },
|
|
195
|
+
h("p", { className: "mkt_panelTitle" }, "任务"),
|
|
196
|
+
props.onClear ? h("span", { className: "mkt_link", onClick: props.onClear }, "清空") : null
|
|
197
|
+
),
|
|
185
198
|
ids.map(function (id) {
|
|
186
199
|
var job = props.jobs[id];
|
|
187
200
|
var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
|
|
@@ -338,11 +351,16 @@ window.__ModuleLoader__.load({
|
|
|
338
351
|
// 无限滚动:哨兵进入视口时拉下一页并追加。GitHub topic 的翻页间数据
|
|
339
352
|
// 可能移动造成重复,按 fullName 去重;已显示数追上 total 即到底。
|
|
340
353
|
var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
|
|
354
|
+
var loadMoreLock = useRef(false);
|
|
341
355
|
var loadMore = useCallback(function () {
|
|
342
356
|
if (!canLoadMore || loading || loadingMore) return;
|
|
357
|
+
// 同一 tick 内 observer 可能连续触发两次,state 更新不阻塞闭包读值,
|
|
358
|
+
// 用 ref 做重入闸。
|
|
359
|
+
if (loadMoreLock.current) return;
|
|
343
360
|
// 限流熔断:失败后 60s 内不再自动请求(哨兵重渲染会反复触发
|
|
344
361
|
// IntersectionObserver,不熔断会连环 500 直到限流窗口过去)。
|
|
345
362
|
if (Date.now() < retryAt) return;
|
|
363
|
+
loadMoreLock.current = true;
|
|
346
364
|
setLoadingMore(true);
|
|
347
365
|
call("search", { query: query, sort: sort, perPage: 20, page: page + 1 }).then(function (value) {
|
|
348
366
|
if (value.truncated === true) { setReachedLimit(true); return; }
|
|
@@ -363,6 +381,7 @@ window.__ModuleLoader__.load({
|
|
|
363
381
|
setError(errorText(e));
|
|
364
382
|
setRetryAt(Date.now() + 60000);
|
|
365
383
|
}).finally(function () {
|
|
384
|
+
loadMoreLock.current = false;
|
|
366
385
|
setLoadingMore(false);
|
|
367
386
|
});
|
|
368
387
|
}, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt]);
|
|
@@ -393,6 +412,7 @@ window.__ModuleLoader__.load({
|
|
|
393
412
|
var polling = useJobPolling(call, refreshInstalled);
|
|
394
413
|
var track = polling.track;
|
|
395
414
|
var jobs = polling.jobs;
|
|
415
|
+
var clearJobs = polling.clear;
|
|
396
416
|
|
|
397
417
|
useEffect(function () {
|
|
398
418
|
doSearch();
|
|
@@ -437,7 +457,12 @@ window.__ModuleLoader__.load({
|
|
|
437
457
|
var tries = 0;
|
|
438
458
|
var ping = setInterval(function () {
|
|
439
459
|
tries++;
|
|
440
|
-
if (tries > 40) {
|
|
460
|
+
if (tries > 40) {
|
|
461
|
+
clearInterval(ping);
|
|
462
|
+
setRestarting(false);
|
|
463
|
+
setError("2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
441
466
|
rpc.call("/market", "installed", {}).then(function () {
|
|
442
467
|
clearInterval(ping);
|
|
443
468
|
window.location.reload();
|
|
@@ -472,16 +497,15 @@ window.__ModuleLoader__.load({
|
|
|
472
497
|
refreshInstalled();
|
|
473
498
|
}, [refreshInstalled]);
|
|
474
499
|
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
for (var jid in jobs) { if (jobs[jid] && jobs[jid].status === "running") { jobsActive = true; break; } }
|
|
478
|
-
|
|
500
|
+
// 任务面板位置固定(已装面板之后):不随任务状态在顶部/底部之间
|
|
501
|
+
// 跳——npm 安装几秒即完成,"完成即落底"曾让结果凭空消失。
|
|
479
502
|
// "只看已验证"开关下的可见项:verify 判定 bundle/client 的才算插件。
|
|
480
503
|
// verifyPending:当前加载的仓库里还有未验证完的(verified map 尚未覆盖)。
|
|
481
504
|
var visibleItems = results === null ? [] : results.items.filter(function (it) {
|
|
482
505
|
if (verifiedOnly !== true) return true;
|
|
483
506
|
var v = verified[it.fullName];
|
|
484
|
-
return v !== undefined && (v.kind === "bundle" || v.kind === "client")
|
|
507
|
+
return v !== undefined && (v.kind === "bundle" || v.kind === "client")
|
|
508
|
+
&& !(v.hostDeps !== undefined && v.hostDeps.length > 0);
|
|
485
509
|
});
|
|
486
510
|
var verifyPending = results !== null && results.items.some(function (it) { return verified[it.fullName] === undefined; });
|
|
487
511
|
|
|
@@ -516,7 +540,7 @@ window.__ModuleLoader__.load({
|
|
|
516
540
|
),
|
|
517
541
|
error ? h("div", { className: "mkt_error" }, error) : null,
|
|
518
542
|
h(InstalledPanel, { installed: installed, removing: removing, updates: updates, onUninstall: doUninstall, onInstallSpec: doInstallSpec }),
|
|
519
|
-
|
|
543
|
+
h(JobsPanel, { jobs: jobs, onClear: clearJobs }),
|
|
520
544
|
h("div", { className: "mkt_list" },
|
|
521
545
|
results == null
|
|
522
546
|
? h("div", { className: "mkt_meta mkt_listHead" }, loading ? "正在加载最热插件…" : "—")
|
|
@@ -552,8 +576,7 @@ window.__ModuleLoader__.load({
|
|
|
552
576
|
? h("div", { className: "mkt_loadMore", ref: sentinelRef }, loadingMore ? "加载中…" : Date.now() < retryAt ? "GitHub 限流中,稍后再下滑加载" : "下滑加载更多")
|
|
553
577
|
: h("div", { className: "mkt_loadMore" }, reachedLimit ? "已达 GitHub 搜索上限(前 1000 个结果)" : "已显示全部 " + results.items.length + " 个")
|
|
554
578
|
)
|
|
555
|
-
)
|
|
556
|
-
jobsActive ? null : h(JobsPanel, { jobs: jobs })
|
|
579
|
+
)
|
|
557
580
|
);
|
|
558
581
|
}
|
|
559
582
|
|
package/src/github.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// GitHub API helpers for the dsh plugin marketplace.
|
|
2
2
|
// Pure functions with no harness imports, so this module is unit-testable
|
|
3
3
|
// standalone (node src/github.js --self-test).
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
4
6
|
|
|
5
7
|
const SEARCH_TOPIC = "topic:dsh-plugin";
|
|
6
8
|
/** GitHub search never serves past the first 1000 results. */
|
|
@@ -116,34 +118,41 @@ export async function searchPlugins({ query, sort = "stars", perPage = 10, page
|
|
|
116
118
|
// to the explicit github: spec).
|
|
117
119
|
|
|
118
120
|
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
119
|
-
const npmCache = new Map(); // name -> {
|
|
121
|
+
const npmCache = new Map(); // name -> {info, at} — null misses expire after 5 min
|
|
120
122
|
|
|
121
123
|
/**
|
|
122
|
-
* Look up a package on the npm registry (abbreviated metadata).
|
|
123
|
-
* the process lifetime;
|
|
124
|
+
* Look up a package on the npm registry (abbreviated metadata). Successful
|
|
125
|
+
* lookups cache for the process lifetime; "not found / unreachable" (null)
|
|
126
|
+
* expires after 5 minutes so a transient registry failure self-heals.
|
|
124
127
|
*/
|
|
125
128
|
export async function npmPackageInfo(name) {
|
|
126
129
|
const clean = String(name ?? "").trim();
|
|
127
130
|
if (clean.length === 0 || !/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i.test(clean)) return null;
|
|
128
|
-
|
|
131
|
+
const cached = npmCache.get(clean);
|
|
132
|
+
if (cached !== undefined && (cached.info !== null || Date.now() - cached.at < 300000)) return cached.info;
|
|
129
133
|
let info = null;
|
|
130
134
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
// 单版本端点返回 latest 的完整 manifest(dependencies + repository 都在)。
|
|
136
|
+
// abbreviated packument 不含 repository,防抢注比对会永远落空。
|
|
137
|
+
const response = await fetch(`${NPM_REGISTRY}/${clean.replace("/", "%2F")}/latest`, {
|
|
138
|
+
headers: { "User-Agent": "dsh-plugin-mall", Accept: "application/json" },
|
|
133
139
|
});
|
|
134
140
|
if (response.ok) {
|
|
135
141
|
const body = await response.json();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const rawRepository = body?.repository;
|
|
142
|
+
if (typeof body?.version === "string") {
|
|
143
|
+
const rawRepository = body.repository;
|
|
139
144
|
const repositoryUrl = typeof rawRepository === "string" ? rawRepository : rawRepository?.url;
|
|
140
|
-
info = {
|
|
145
|
+
info = {
|
|
146
|
+
latest: body.version,
|
|
147
|
+
repositoryUrl: typeof repositoryUrl === "string" ? repositoryUrl : undefined,
|
|
148
|
+
hostDeps: hostShadowDependencies(body),
|
|
149
|
+
};
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
} catch {
|
|
144
153
|
info = null; // registry unreachable — caller falls back
|
|
145
154
|
}
|
|
146
|
-
npmCache.set(clean, info);
|
|
155
|
+
npmCache.set(clean, { info, at: Date.now() });
|
|
147
156
|
return info;
|
|
148
157
|
}
|
|
149
158
|
|
|
@@ -166,7 +175,64 @@ export async function preferNpmSpec({ spec }) {
|
|
|
166
175
|
return pointsBack ? declaredName : raw;
|
|
167
176
|
}
|
|
168
177
|
|
|
169
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Refuse to install a package whose `dependencies` shadow host framework
|
|
180
|
+
* packages. Host copies inside a profile split module identities and crash
|
|
181
|
+
* all tool scheduling — the dsh contract is peerDependencies, but the host
|
|
182
|
+
* enforces nothing, so the marketplace is the last line of defense.
|
|
183
|
+
* Sources: registry abbreviated metadata for npm names, the verify cache
|
|
184
|
+
* (raw package.json) for github: specs.
|
|
185
|
+
*/
|
|
186
|
+
/**
|
|
187
|
+
* Extract the bare npm package name: "name", "name@version", "@s/n@version"
|
|
188
|
+
* → "name"/"@s/n". Returns null for anything that is not an npm name shape.
|
|
189
|
+
*/
|
|
190
|
+
function npmNameOf(raw) {
|
|
191
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
192
|
+
if (raw.startsWith("@")) {
|
|
193
|
+
const match = /^(@[^/@\s]+\/[^/@\s]+?)(?:@[^/\s]+)?$/.exec(raw);
|
|
194
|
+
return match === null ? null : match[1];
|
|
195
|
+
}
|
|
196
|
+
const match = /^([^@\s]+?)(?:@[^/\s]+)?$/.exec(raw);
|
|
197
|
+
return match === null ? null : match[1];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function assertSafeToInstall({ spec }) {
|
|
201
|
+
const raw = String(spec ?? "");
|
|
202
|
+
let hostDeps;
|
|
203
|
+
if (/^(?:file:|link:)/i.test(raw)) {
|
|
204
|
+
// 本地路径直通:直接读它的 package.json 查 dependencies。
|
|
205
|
+
const dir = raw.replace(/^(?:file:|link:)/i, "").replace(/[\\/]+$/, "");
|
|
206
|
+
try {
|
|
207
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
208
|
+
hostDeps = hostShadowDependencies(pkg);
|
|
209
|
+
} catch {
|
|
210
|
+
return; // 读不到清单——pnpm 会给出真实错误,这里放行
|
|
211
|
+
}
|
|
212
|
+
} else if (/^github:([^/\s]+\/[^/\s]+?)(?:\.git)?(?:#.+)?$/i.test(raw)) {
|
|
213
|
+
const repo = /^github:([^/\s]+\/[^/\s]+?)(?:\.git)?(?:#.+)?$/i.exec(raw)[1];
|
|
214
|
+
const { results } = await verifyPlugins({ repos: [repo] });
|
|
215
|
+
hostDeps = results[repo]?.hostDeps;
|
|
216
|
+
} else if (/^https?:\/\//i.test(raw)) {
|
|
217
|
+
return; // 远程 tarball 下载前无法廉价检查;罕见路径,放行
|
|
218
|
+
} else {
|
|
219
|
+
// npm 名(含带版本形式):剥掉 @version 再查。
|
|
220
|
+
const name = npmNameOf(raw);
|
|
221
|
+
if (name === null || name.length === 0) {
|
|
222
|
+
// 无法识别形状的 spec 一律拒绝,而不是静默跳过检查。
|
|
223
|
+
throw new Error(`cannot analyze install spec ${JSON.stringify(raw)} for host-shadow dependencies — refusing to install`);
|
|
224
|
+
}
|
|
225
|
+
const info = await npmPackageInfo(name);
|
|
226
|
+
hostDeps = info?.hostDeps;
|
|
227
|
+
}
|
|
228
|
+
if (hostDeps !== undefined && hostDeps !== null && hostDeps.length > 0) {
|
|
229
|
+
throw new Error(`${raw} declares ${hostDeps.length} host framework package(s) as dependencies (${hostDeps.slice(0, 3).join(", ")}${hostDeps.length > 3 ? ", …" : ""}) — installing it would duplicate host modules and crash dsh tool scheduling. Ask the plugin author to move @deepseek-ai/* to peerDependencies.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Loose semver-ish comparison: "0.2.10" vs "0.2.9" → 1. Non-numeric parts
|
|
234
|
+
* read as 0; numerically equal versions where one carries a pre-release
|
|
235
|
+
* segment ("-rc.1") sort below the plain release ("2.0.0-beta" < "2.0.0"). */
|
|
170
236
|
export function compareVersions(a, b) {
|
|
171
237
|
const pa = String(a ?? "").split(".");
|
|
172
238
|
const pb = String(b ?? "").split(".");
|
|
@@ -175,6 +241,9 @@ export function compareVersions(a, b) {
|
|
|
175
241
|
const nb = Number(pb[index]) || 0;
|
|
176
242
|
if (na !== nb) return na < nb ? -1 : 1;
|
|
177
243
|
}
|
|
244
|
+
const preA = String(a ?? "").includes("-");
|
|
245
|
+
const preB = String(b ?? "").includes("-");
|
|
246
|
+
if (preA !== preB) return preA ? -1 : 1;
|
|
178
247
|
return 0;
|
|
179
248
|
}
|
|
180
249
|
// ── plugin verification (raw CDN, no API quota) ─────────────────────────────
|
|
@@ -222,15 +291,37 @@ async function fetchRawPackageJson(repo, signal) {
|
|
|
222
291
|
throw lastError ?? new Error("no raw source reachable");
|
|
223
292
|
}
|
|
224
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Framework packages the host provides via the profiles/node_modules fallback.
|
|
296
|
+
* A plugin declaring any of these as `dependencies` (instead of
|
|
297
|
+
* peerDependencies) installs real copies into the profile — the loader then
|
|
298
|
+
* holds two module instances, symbol identities split, and every tool call
|
|
299
|
+
* crashes with an undiagnosable "reading 'prepare'". See installer docs.
|
|
300
|
+
*/
|
|
301
|
+
const HOST_PACKAGES = /^(@deepseek-ai\/|cosmokit$|schemastery$)/;
|
|
302
|
+
|
|
303
|
+
/** The @deepseek-ai/* (and cordis runtime) entries inside `dependencies`. */
|
|
304
|
+
function hostShadowDependencies(pkg) {
|
|
305
|
+
if (pkg === undefined || typeof pkg !== "object") return undefined;
|
|
306
|
+
const deps = Object.keys(pkg.dependencies ?? {});
|
|
307
|
+
const host = deps.filter((name) => HOST_PACKAGES.test(name));
|
|
308
|
+
return host.length > 0 ? host : undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
225
311
|
/**
|
|
226
312
|
* Verify repositories as real dsh plugins by their package.json declaration.
|
|
227
313
|
* @param {{repos: string[], signal?: AbortSignal}} options - "owner/name" list.
|
|
228
|
-
* @returns the {results} map: fullName -> {kind: "bundle"|"client"|"plain"|"no-manifest"|"unknown", name?, version?}.
|
|
314
|
+
* @returns the {results} map: fullName -> {kind: "bundle"|"client"|"plain"|"no-manifest"|"unknown", name?, version?, hostDeps?}.
|
|
229
315
|
*/
|
|
230
316
|
export async function verifyPlugins({ repos, signal }) {
|
|
231
317
|
const wanted = [...new Set((Array.isArray(repos) ? repos : []).map(String)
|
|
232
318
|
.filter((repo) => /^[^/\s]+\/[^/\s]+$/.test(repo) && !repo.includes("..")))];
|
|
233
|
-
|
|
319
|
+
// "unknown"(网络失败)60s 后过期重试;成功结果进程内永久缓存。
|
|
320
|
+
const pending = wanted.filter((repo) => {
|
|
321
|
+
const cached = verifyCache.get(repo);
|
|
322
|
+
if (cached === undefined) return true;
|
|
323
|
+
return cached.kind === "unknown" && Date.now() - (cached.ts ?? 0) > 60000;
|
|
324
|
+
});
|
|
234
325
|
let cursor = 0;
|
|
235
326
|
const worker = async () => {
|
|
236
327
|
while (cursor < pending.length) {
|
|
@@ -241,16 +332,21 @@ export async function verifyPlugins({ repos, signal }) {
|
|
|
241
332
|
: typeof pkg.dsh?.bundle?.patch === "string" ? "bundle"
|
|
242
333
|
: pkg.dsh?.client !== undefined ? "client"
|
|
243
334
|
: "plain";
|
|
244
|
-
verifyCache.set(repo, { kind, name: pkg?.name, version: pkg?.version });
|
|
335
|
+
verifyCache.set(repo, { kind, name: pkg?.name, version: pkg?.version, hostDeps: hostShadowDependencies(pkg) });
|
|
245
336
|
} catch (error) {
|
|
246
337
|
if (error?.name === "AbortError") throw error;
|
|
247
|
-
verifyCache.set(repo, { kind: "unknown" });
|
|
338
|
+
verifyCache.set(repo, { kind: "unknown", ts: Date.now() });
|
|
248
339
|
}
|
|
249
340
|
}
|
|
250
341
|
};
|
|
251
342
|
await Promise.all(Array.from({ length: Math.min(VERIFY_CONCURRENCY, pending.length) }, worker));
|
|
252
343
|
const results = {};
|
|
253
|
-
for (const repo of wanted)
|
|
344
|
+
for (const repo of wanted) {
|
|
345
|
+
const cached = verifyCache.get(repo);
|
|
346
|
+
results[repo] = cached === undefined || cached.ts !== undefined
|
|
347
|
+
? { kind: cached?.kind ?? "unknown", name: cached?.name, version: cached?.version, hostDeps: cached?.hostDeps }
|
|
348
|
+
: cached;
|
|
349
|
+
}
|
|
254
350
|
return { results };
|
|
255
351
|
}
|
|
256
352
|
|
package/src/index.js
CHANGED
|
@@ -19,8 +19,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
21
|
import { resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
|
|
22
|
-
import { repoInfo, searchPlugins, verifyPlugins, preferNpmSpec, npmPackageInfo, compareVersions } from "./github.js";
|
|
23
|
-
import { ensureProfile, listInstalled, normalizeSpec, runInstall, runRemove, createJobTracker } from "./installer.js";
|
|
22
|
+
import { repoInfo, searchPlugins, verifyPlugins, preferNpmSpec, npmPackageInfo, compareVersions, assertSafeToInstall } from "./github.js";
|
|
23
|
+
import { ensureProfile, listInstalled, normalizeSpec, runInstall, runRemove, createJobTracker, assertSafeSpec } from "./installer.js";
|
|
24
24
|
|
|
25
25
|
export const name = "@1e0zj/dsh-plugin-mall";
|
|
26
26
|
export const inject = ["tools", "jobs", "systemPrompt"];
|
|
@@ -206,12 +206,20 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
206
206
|
let spec;
|
|
207
207
|
try {
|
|
208
208
|
spec = normalizeSpec(payload?.spec);
|
|
209
|
+
assertSafeSpec(spec);
|
|
209
210
|
} catch (error) {
|
|
210
211
|
return rpcFail(error);
|
|
211
212
|
}
|
|
212
213
|
// npm tarball 优先(小而快、带 integrity);registry 条目不同源的包名
|
|
213
214
|
// 视为抢注,回退 github: 全仓库 spec。
|
|
214
215
|
spec = await preferNpmSpec({ spec });
|
|
216
|
+
// 宿主依赖硬拦:dependencies 里拖着 @deepseek-ai/* 的包装进 profile
|
|
217
|
+
// 就是双模块实例 + 全工具调度崩溃(宿主无任何护栏,市场是最后防线)。
|
|
218
|
+
try {
|
|
219
|
+
await assertSafeToInstall({ spec });
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return rpcFail(error);
|
|
222
|
+
}
|
|
215
223
|
try {
|
|
216
224
|
const profileDir = resolveProfileDir(profile);
|
|
217
225
|
if (!existsSync(join(profileDir, "package.json"))) ensureProfile(profile);
|
|
@@ -229,6 +237,11 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
229
237
|
const profile = String(payload?.profile ?? defaultProfile).trim();
|
|
230
238
|
const packageName = String(payload?.package ?? "").trim();
|
|
231
239
|
if (packageName.length === 0) return rpcFail(new Error("uninstall: package name is required"));
|
|
240
|
+
try {
|
|
241
|
+
assertSafeSpec(packageName);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return rpcFail(error);
|
|
244
|
+
}
|
|
232
245
|
try {
|
|
233
246
|
const profileDir = resolveProfileDir(profile);
|
|
234
247
|
if (!existsSync(join(profileDir, "package.json"))) {
|
|
@@ -267,7 +280,7 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
267
280
|
: `sleep 2 && ${relaunch}`;
|
|
268
281
|
const child = spawn(launcher, { shell: true, detached: true, stdio: "ignore", cwd: process.cwd(), windowsHide: true });
|
|
269
282
|
child.unref();
|
|
270
|
-
setTimeout(() => process.exit(0),
|
|
283
|
+
setTimeout(() => process.exit(0), 1500);
|
|
271
284
|
return rpcOk({ restarting: true });
|
|
272
285
|
}
|
|
273
286
|
case "jobCancel": {
|
|
@@ -418,7 +431,10 @@ export function apply(ctx, config = {}) {
|
|
|
418
431
|
},
|
|
419
432
|
async execute(args, exec) {
|
|
420
433
|
const profile = String(args.profile ?? defaultProfile).trim();
|
|
421
|
-
const
|
|
434
|
+
const normalized = normalizeSpec(args.spec);
|
|
435
|
+
assertSafeSpec(normalized);
|
|
436
|
+
const spec = await preferNpmSpec({ spec: normalized });
|
|
437
|
+
await assertSafeToInstall({ spec });
|
|
422
438
|
let profileDir;
|
|
423
439
|
try {
|
|
424
440
|
profileDir = resolveProfileDir(profile);
|
|
@@ -473,6 +489,7 @@ export function apply(ctx, config = {}) {
|
|
|
473
489
|
const profile = String(args.profile ?? defaultProfile).trim();
|
|
474
490
|
const packageName = String(args.package ?? "").trim();
|
|
475
491
|
if (packageName.length === 0) throw new Error("market_uninstall: package name is required");
|
|
492
|
+
assertSafeSpec(packageName);
|
|
476
493
|
try {
|
|
477
494
|
resolveProfileDir(profile);
|
|
478
495
|
} catch (error) {
|
package/src/installer.js
CHANGED
|
@@ -361,6 +361,20 @@ export function createJobTracker() {
|
|
|
361
361
|
};
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
// ── spec shape guard ────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
// Windows spawn 走 shell,spec 会被拼进 cmd 行;agent 传入的参数不可信。
|
|
367
|
+
// 合法的 npm 名 / github:owner\/repo / git·file·link·URL spec 都不含这些
|
|
368
|
+
// shell 元字符——出现即拒绝,宁可误杀不放开命令注入面。
|
|
369
|
+
const UNSAFE_SPEC_RE = /[;&|`$()<>^"!*\n\r]/;
|
|
370
|
+
|
|
371
|
+
/** Reject install/remove specs carrying shell metacharacters. */
|
|
372
|
+
export function assertSafeSpec(spec) {
|
|
373
|
+
if (UNSAFE_SPEC_RE.test(String(spec ?? ""))) {
|
|
374
|
+
throw new Error(`spec contains characters that are not allowed in an install spec: ${JSON.stringify(String(spec))}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
364
378
|
// ── pnpm self-heal (corepack) ───────────────────────────────────────────────
|
|
365
379
|
|
|
366
380
|
/**
|
|
@@ -565,10 +579,12 @@ export function runRemove({ profile, packageName }, selfHealed = false) {
|
|
|
565
579
|
proc.on("close", (exitCode) => resolve({ exitCode, signal: proc.signalCode }));
|
|
566
580
|
}).then(async (outcome) => {
|
|
567
581
|
if (outcome.spawnError !== undefined) {
|
|
568
|
-
// pnpm 缺失时先 corepack 自愈一次再重试(重试在新 producer
|
|
582
|
+
// pnpm 缺失时先 corepack 自愈一次再重试(重试在新 producer 里跑,
|
|
583
|
+
// 这里必须返回它的 done outcome——返回 producer 本体会让 tracker
|
|
584
|
+
// 把成功任务记成 failed)。
|
|
569
585
|
if (outcome.spawnError.code === "ENOENT" && selfHealed !== true) {
|
|
570
|
-
const healed = await enablePnpmViaCorepack(
|
|
571
|
-
if (healed) return runRemove({ profile, packageName }, true);
|
|
586
|
+
const healed = await enablePnpmViaCorepack(push);
|
|
587
|
+
if (healed) return await runRemove({ profile, packageName }, true).done;
|
|
572
588
|
}
|
|
573
589
|
const hint = outcome.spawnError.code === "ENOENT"
|
|
574
590
|
? "pnpm not found on PATH — install pnpm (e.g. `corepack enable pnpm`) to manage profile plugins"
|