@1e0zj/dsh-plugin-mall 0.1.11 → 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 +12 -1
- package/src/github.js +76 -23
- package/src/index.js +12 -3
- package/src/installer.js +19 -3
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -351,11 +351,16 @@ window.__ModuleLoader__.load({
|
|
|
351
351
|
// 无限滚动:哨兵进入视口时拉下一页并追加。GitHub topic 的翻页间数据
|
|
352
352
|
// 可能移动造成重复,按 fullName 去重;已显示数追上 total 即到底。
|
|
353
353
|
var canLoadMore = results !== null && results.items.length < results.total && !reachedLimit;
|
|
354
|
+
var loadMoreLock = useRef(false);
|
|
354
355
|
var loadMore = useCallback(function () {
|
|
355
356
|
if (!canLoadMore || loading || loadingMore) return;
|
|
357
|
+
// 同一 tick 内 observer 可能连续触发两次,state 更新不阻塞闭包读值,
|
|
358
|
+
// 用 ref 做重入闸。
|
|
359
|
+
if (loadMoreLock.current) return;
|
|
356
360
|
// 限流熔断:失败后 60s 内不再自动请求(哨兵重渲染会反复触发
|
|
357
361
|
// IntersectionObserver,不熔断会连环 500 直到限流窗口过去)。
|
|
358
362
|
if (Date.now() < retryAt) return;
|
|
363
|
+
loadMoreLock.current = true;
|
|
359
364
|
setLoadingMore(true);
|
|
360
365
|
call("search", { query: query, sort: sort, perPage: 20, page: page + 1 }).then(function (value) {
|
|
361
366
|
if (value.truncated === true) { setReachedLimit(true); return; }
|
|
@@ -376,6 +381,7 @@ window.__ModuleLoader__.load({
|
|
|
376
381
|
setError(errorText(e));
|
|
377
382
|
setRetryAt(Date.now() + 60000);
|
|
378
383
|
}).finally(function () {
|
|
384
|
+
loadMoreLock.current = false;
|
|
379
385
|
setLoadingMore(false);
|
|
380
386
|
});
|
|
381
387
|
}, [call, canLoadMore, loading, loadingMore, page, query, sort, retryAt]);
|
|
@@ -451,7 +457,12 @@ window.__ModuleLoader__.load({
|
|
|
451
457
|
var tries = 0;
|
|
452
458
|
var ping = setInterval(function () {
|
|
453
459
|
tries++;
|
|
454
|
-
if (tries > 40) {
|
|
460
|
+
if (tries > 40) {
|
|
461
|
+
clearInterval(ping);
|
|
462
|
+
setRestarting(false);
|
|
463
|
+
setError("2 分钟内未检测到 dsh 重启完成,请手动检查 dsh 状态后刷新页面。");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
455
466
|
rpc.call("/market", "installed", {}).then(function () {
|
|
456
467
|
clearInterval(ping);
|
|
457
468
|
window.location.reload();
|
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
|
|
|
@@ -174,25 +183,56 @@ export async function preferNpmSpec({ spec }) {
|
|
|
174
183
|
* Sources: registry abbreviated metadata for npm names, the verify cache
|
|
175
184
|
* (raw package.json) for github: specs.
|
|
176
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
|
+
|
|
177
200
|
export async function assertSafeToInstall({ spec }) {
|
|
178
201
|
const raw = String(spec ?? "");
|
|
179
202
|
let hostDeps;
|
|
180
|
-
if (/^(
|
|
181
|
-
|
|
182
|
-
|
|
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 下载前无法廉价检查;罕见路径,放行
|
|
183
218
|
} else {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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`);
|
|
188
224
|
}
|
|
225
|
+
const info = await npmPackageInfo(name);
|
|
226
|
+
hostDeps = info?.hostDeps;
|
|
189
227
|
}
|
|
190
|
-
if (hostDeps !== undefined && hostDeps.length > 0) {
|
|
228
|
+
if (hostDeps !== undefined && hostDeps !== null && hostDeps.length > 0) {
|
|
191
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.`);
|
|
192
230
|
}
|
|
193
231
|
}
|
|
194
232
|
|
|
195
|
-
/** Loose semver-ish comparison: "0.2.10" vs "0.2.9" → 1. Non-numeric parts
|
|
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"). */
|
|
196
236
|
export function compareVersions(a, b) {
|
|
197
237
|
const pa = String(a ?? "").split(".");
|
|
198
238
|
const pb = String(b ?? "").split(".");
|
|
@@ -201,6 +241,9 @@ export function compareVersions(a, b) {
|
|
|
201
241
|
const nb = Number(pb[index]) || 0;
|
|
202
242
|
if (na !== nb) return na < nb ? -1 : 1;
|
|
203
243
|
}
|
|
244
|
+
const preA = String(a ?? "").includes("-");
|
|
245
|
+
const preB = String(b ?? "").includes("-");
|
|
246
|
+
if (preA !== preB) return preA ? -1 : 1;
|
|
204
247
|
return 0;
|
|
205
248
|
}
|
|
206
249
|
// ── plugin verification (raw CDN, no API quota) ─────────────────────────────
|
|
@@ -273,7 +316,12 @@ function hostShadowDependencies(pkg) {
|
|
|
273
316
|
export async function verifyPlugins({ repos, signal }) {
|
|
274
317
|
const wanted = [...new Set((Array.isArray(repos) ? repos : []).map(String)
|
|
275
318
|
.filter((repo) => /^[^/\s]+\/[^/\s]+$/.test(repo) && !repo.includes("..")))];
|
|
276
|
-
|
|
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
|
+
});
|
|
277
325
|
let cursor = 0;
|
|
278
326
|
const worker = async () => {
|
|
279
327
|
while (cursor < pending.length) {
|
|
@@ -287,13 +335,18 @@ export async function verifyPlugins({ repos, signal }) {
|
|
|
287
335
|
verifyCache.set(repo, { kind, name: pkg?.name, version: pkg?.version, hostDeps: hostShadowDependencies(pkg) });
|
|
288
336
|
} catch (error) {
|
|
289
337
|
if (error?.name === "AbortError") throw error;
|
|
290
|
-
verifyCache.set(repo, { kind: "unknown" });
|
|
338
|
+
verifyCache.set(repo, { kind: "unknown", ts: Date.now() });
|
|
291
339
|
}
|
|
292
340
|
}
|
|
293
341
|
};
|
|
294
342
|
await Promise.all(Array.from({ length: Math.min(VERIFY_CONCURRENCY, pending.length) }, worker));
|
|
295
343
|
const results = {};
|
|
296
|
-
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
|
+
}
|
|
297
350
|
return { results };
|
|
298
351
|
}
|
|
299
352
|
|
package/src/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { join } from "node:path";
|
|
|
20
20
|
import { spawn } from "node:child_process";
|
|
21
21
|
import { resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
|
|
22
22
|
import { repoInfo, searchPlugins, verifyPlugins, preferNpmSpec, npmPackageInfo, compareVersions, assertSafeToInstall } from "./github.js";
|
|
23
|
-
import { ensureProfile, listInstalled, normalizeSpec, runInstall, runRemove, createJobTracker } from "./installer.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,6 +206,7 @@ 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
|
}
|
|
@@ -236,6 +237,11 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
236
237
|
const profile = String(payload?.profile ?? defaultProfile).trim();
|
|
237
238
|
const packageName = String(payload?.package ?? "").trim();
|
|
238
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
|
+
}
|
|
239
245
|
try {
|
|
240
246
|
const profileDir = resolveProfileDir(profile);
|
|
241
247
|
if (!existsSync(join(profileDir, "package.json"))) {
|
|
@@ -274,7 +280,7 @@ async function rpcDispatch(ctx, endpoint, payload, config, token, tracker) {
|
|
|
274
280
|
: `sleep 2 && ${relaunch}`;
|
|
275
281
|
const child = spawn(launcher, { shell: true, detached: true, stdio: "ignore", cwd: process.cwd(), windowsHide: true });
|
|
276
282
|
child.unref();
|
|
277
|
-
setTimeout(() => process.exit(0),
|
|
283
|
+
setTimeout(() => process.exit(0), 1500);
|
|
278
284
|
return rpcOk({ restarting: true });
|
|
279
285
|
}
|
|
280
286
|
case "jobCancel": {
|
|
@@ -425,7 +431,9 @@ export function apply(ctx, config = {}) {
|
|
|
425
431
|
},
|
|
426
432
|
async execute(args, exec) {
|
|
427
433
|
const profile = String(args.profile ?? defaultProfile).trim();
|
|
428
|
-
const
|
|
434
|
+
const normalized = normalizeSpec(args.spec);
|
|
435
|
+
assertSafeSpec(normalized);
|
|
436
|
+
const spec = await preferNpmSpec({ spec: normalized });
|
|
429
437
|
await assertSafeToInstall({ spec });
|
|
430
438
|
let profileDir;
|
|
431
439
|
try {
|
|
@@ -481,6 +489,7 @@ export function apply(ctx, config = {}) {
|
|
|
481
489
|
const profile = String(args.profile ?? defaultProfile).trim();
|
|
482
490
|
const packageName = String(args.package ?? "").trim();
|
|
483
491
|
if (packageName.length === 0) throw new Error("market_uninstall: package name is required");
|
|
492
|
+
assertSafeSpec(packageName);
|
|
484
493
|
try {
|
|
485
494
|
resolveProfileDir(profile);
|
|
486
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"
|