@1e0zj/dsh-plugin-mall 0.4.2 → 0.4.4
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 +8 -5
- package/src/github.js +145 -5
- package/src/index.js +875 -149
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -499,10 +499,10 @@ window.__ModuleLoader__.load({
|
|
|
499
499
|
busy: props.approving === job.spec,
|
|
500
500
|
onApprove: function (names) {
|
|
501
501
|
// 不先 drop:旧条目由重试任务的 track(carryFromId) 原子接管
|
|
502
|
-
// (撤条目 +
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
502
|
+
// (撤条目 + 日志接续一拍完成)。install RPC 现在只做本地校验、
|
|
503
|
+
// 立刻返回新 job id,但保留这个次序仍然是它最稳的形态——
|
|
504
|
+
// 万一 RPC 失败,旧条目和日志还在。等待期间按钮由 approving
|
|
505
|
+
// 态显示「继续中…」。
|
|
506
506
|
props.onApprove(job.spec, names, job.approvalToken, id);
|
|
507
507
|
},
|
|
508
508
|
onDismiss: function () { props.onDismiss(id); },
|
|
@@ -1056,7 +1056,10 @@ window.__ModuleLoader__.load({
|
|
|
1056
1056
|
var spec = preflight.spec;
|
|
1057
1057
|
var carry = preflight.jobId;
|
|
1058
1058
|
setPreflight(null);
|
|
1059
|
-
|
|
1059
|
+
// consentDigest 来自预检 job 的 extras,装的时候与当前报告比对:
|
|
1060
|
+
// 用户点「继续」到安装真正开跑之间候选包或 profile 变了,
|
|
1061
|
+
// 布尔同意不得沿用,要重新看新的警告。
|
|
1062
|
+
doRawInstall(spec, { acceptWarnings: true, acceptedReportDigest: preflight.report.consentDigest }, carry);
|
|
1060
1063
|
},
|
|
1061
1064
|
onClose: function () {
|
|
1062
1065
|
if (preflight && preflight.spec) delete approvalTokensRef.current[preflight.spec];
|
package/src/github.js
CHANGED
|
@@ -64,7 +64,29 @@ async function requestJson(path, { apiBase, token, signal }) {
|
|
|
64
64
|
}
|
|
65
65
|
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
66
66
|
const resetAt = response.headers.get("x-ratelimit-reset");
|
|
67
|
-
|
|
67
|
+
// 读体阶段的三种失败必须分开,不能一把 catch 成 undefined:
|
|
68
|
+
// 调用方取消(响应头已到、body 读到一半浏览器断开)→ AbortError 上抛;
|
|
69
|
+
// 内部超时打断读体 → 与请求阶段超时同待遇,退避后重试;
|
|
70
|
+
// 响应体根本不是 JSON(镜像的 HTML 错误页等)→ 维持 undefined,交给
|
|
71
|
+
// 下面的状态码分支。
|
|
72
|
+
// 此前这里是无差别 .catch(() => undefined):mid-body 取消被吞掉,200 响应
|
|
73
|
+
// 返回 undefined,search 拿到的是 TypeError 而不是取消;4xx 路径则把一次
|
|
74
|
+
// 取消谎报成 not found。
|
|
75
|
+
let body;
|
|
76
|
+
try {
|
|
77
|
+
body = await response.json();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error?.name === "AbortError" && signal?.aborted) throw error;
|
|
80
|
+
// 读体超时只在成功响应上重试。4xx 的结论在读体之前就已确定
|
|
81
|
+
// (见上方契约:4xx 判定式失败,从不重试)——404 有 404 的报法、
|
|
82
|
+
// 403 限流有专用诊断,读不出 body 不改变任何一件事;重试三次
|
|
83
|
+
// 只会把正确答案换成一句超时。
|
|
84
|
+
if (error?.name === "TimeoutError" && response.ok) {
|
|
85
|
+
lastError = new Error(`GitHub API response body timed out after ${REQUEST_TIMEOUT / 1000}s (attempt ${attempt + 1})`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
body = undefined;
|
|
89
|
+
}
|
|
68
90
|
if (response.status === 403 && remaining === "0" && resetAt !== null) {
|
|
69
91
|
const reset = new Date(Number(resetAt) * 1000).toISOString();
|
|
70
92
|
throw new Error(`GitHub API rate limit exceeded; resets at ${reset} (UTC). Set GITHUB_TOKEN or DSH_MARKET_GITHUB_TOKEN for a higher limit.`);
|
|
@@ -173,16 +195,19 @@ function normalizeRegistry(registry) {
|
|
|
173
195
|
* @param name - the npm package name.
|
|
174
196
|
* @param options - `registry` defaults to npmjs; pass what pnpm installs from.
|
|
175
197
|
* `signal` cancels the request (and, critically, keeps the cancellation out
|
|
176
|
-
* of the cache — see below).
|
|
198
|
+
* of the cache — see below). `fresh` skips the read side of the cache: a
|
|
199
|
+
* cached "current version" is stale by construction, and the preflight
|
|
200
|
+
* staleness check exists precisely to compare against what the registry
|
|
201
|
+
* says NOW (the write side still populates the cache for other callers).
|
|
177
202
|
* @returns `{latest, repositoryUrl, hostDeps}`, or null when unknown/unreachable.
|
|
178
203
|
*/
|
|
179
|
-
export async function npmPackageInfo(name, { registry, signal } = {}) {
|
|
204
|
+
export async function npmPackageInfo(name, { registry, signal, fresh = false } = {}) {
|
|
180
205
|
const clean = String(name ?? "").trim();
|
|
181
206
|
if (clean.length === 0 || !/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i.test(clean)) return null;
|
|
182
207
|
const base = normalizeRegistry(registry);
|
|
183
208
|
const key = `${base}|${clean}`;
|
|
184
209
|
const cached = npmCache.get(key);
|
|
185
|
-
if (cached !== undefined && Date.now() - cached.at < NPM_CACHE_TTL) return cached.info;
|
|
210
|
+
if (fresh !== true && cached !== undefined && Date.now() - cached.at < NPM_CACHE_TTL) return cached.info;
|
|
186
211
|
let info = null;
|
|
187
212
|
try {
|
|
188
213
|
// 单版本端点返回 latest 的完整 manifest(dependencies + repository 都在)。
|
|
@@ -215,6 +240,44 @@ export async function npmPackageInfo(name, { registry, signal } = {}) {
|
|
|
215
240
|
return info;
|
|
216
241
|
}
|
|
217
242
|
|
|
243
|
+
const npmVersionsCache = new Map();
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* All published version strings of a package, oldest to newest, for resolving
|
|
247
|
+
* a RANGE spec ("pkg@^1.2.0") to the version pnpm would actually pick today.
|
|
248
|
+
* `/latest` cannot answer that — a new release inside the range is invisible
|
|
249
|
+
* to it until it becomes latest. Fetches the full packument (one request),
|
|
250
|
+
* which is why this lives behind its own TTL like npmPackageInfo. Same
|
|
251
|
+
* cancellation rule: AbortError is rethrown, never cached as an empty answer.
|
|
252
|
+
* @returns string[], or null when unreachable/unknown.
|
|
253
|
+
*/
|
|
254
|
+
export async function npmPackageVersions(name, { registry, signal, fresh = false } = {}) {
|
|
255
|
+
const clean = String(name ?? "").trim();
|
|
256
|
+
if (clean.length === 0 || !/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/i.test(clean)) return null;
|
|
257
|
+
const base = normalizeRegistry(registry);
|
|
258
|
+
const key = `${base}|${clean}`;
|
|
259
|
+
const cached = npmVersionsCache.get(key);
|
|
260
|
+
if (fresh !== true && cached !== undefined && Date.now() - cached.at < NPM_CACHE_TTL) return cached.versions;
|
|
261
|
+
let versions = null;
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetch(`${base}/${clean.replace("/", "%2F")}`, {
|
|
264
|
+
headers: { "User-Agent": "dsh-plugin-mall", Accept: "application/json" },
|
|
265
|
+
signal,
|
|
266
|
+
});
|
|
267
|
+
if (response.ok) {
|
|
268
|
+
const body = await response.json();
|
|
269
|
+
if (body?.versions !== null && typeof body?.versions === "object") {
|
|
270
|
+
versions = Object.keys(body.versions);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (error?.name === "AbortError") throw error;
|
|
275
|
+
versions = null;
|
|
276
|
+
}
|
|
277
|
+
npmVersionsCache.set(key, { versions, at: Date.now() });
|
|
278
|
+
return versions;
|
|
279
|
+
}
|
|
280
|
+
|
|
218
281
|
/**
|
|
219
282
|
* Rewrite "github:owner/repo" (or "owner/repo") to the npm package name when
|
|
220
283
|
* that package exists on npm AND its repository URL points back at the repo
|
|
@@ -577,6 +640,9 @@ export async function repoInfo({ repo, apiBase, token, signal }) {
|
|
|
577
640
|
try {
|
|
578
641
|
meta = await requestJson(`/repos/${trimmed}`, { apiBase, token, signal });
|
|
579
642
|
} catch (error) {
|
|
643
|
+
// 取消不是「仓库不存在」:包装会把这个信号翻译成业务错误,调用方据此
|
|
644
|
+
// 报告一个不存在的网络故障。取消原样上抛。
|
|
645
|
+
if (error?.name === "AbortError") throw error;
|
|
580
646
|
throw new Error(`market_info: repository ${trimmed} not found on GitHub (${error.message})`);
|
|
581
647
|
}
|
|
582
648
|
let packageJson;
|
|
@@ -585,7 +651,8 @@ export async function repoInfo({ repo, apiBase, token, signal }) {
|
|
|
585
651
|
if (typeof contents.content === "string") {
|
|
586
652
|
packageJson = JSON.parse(Buffer.from(contents.content, "base64").toString("utf8"));
|
|
587
653
|
}
|
|
588
|
-
} catch {
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (error?.name === "AbortError") throw error; // 取消不是「没有 package.json」
|
|
589
656
|
packageJson = undefined; // no package.json at the repo root
|
|
590
657
|
}
|
|
591
658
|
return {
|
|
@@ -687,6 +754,79 @@ if (process.argv[1]?.endsWith("github.js") && process.argv.includes("--self-test
|
|
|
687
754
|
}
|
|
688
755
|
if (clampFailed > 0) process.exit(1);
|
|
689
756
|
}
|
|
757
|
+
// requestJson 读体阶段的三种失败。打桩 fetch——离线、确定性;abort 在
|
|
758
|
+
// json() 内部发生(不是调用前预 abort),逼近「响应头已到、body 读一半
|
|
759
|
+
// 浏览器断开」的真实时序。
|
|
760
|
+
{
|
|
761
|
+
const realFetch = globalThis.fetch;
|
|
762
|
+
const makeError = (name) => { const error = new Error("synthetic"); error.name = name; return error; };
|
|
763
|
+
const fakeResponse = ({ status, headers, json }) => ({
|
|
764
|
+
ok: status < 400,
|
|
765
|
+
status,
|
|
766
|
+
statusText: status === 404 ? "Not Found" : status === 403 ? "Forbidden" : "OK",
|
|
767
|
+
headers: headers ?? new Headers(),
|
|
768
|
+
json,
|
|
769
|
+
});
|
|
770
|
+
let failed = 0;
|
|
771
|
+
try {
|
|
772
|
+
// 1) mid-body 取消:json() 里才 abort。修复前 .catch(()=>undefined)
|
|
773
|
+
// 把它吞掉——200 变 undefined(search 得 TypeError)、4xx 谎报 not found。
|
|
774
|
+
{
|
|
775
|
+
const controller = new AbortController();
|
|
776
|
+
let threw;
|
|
777
|
+
globalThis.fetch = async () => fakeResponse({
|
|
778
|
+
status: 200,
|
|
779
|
+
json: async () => { controller.abort(); throw makeError("AbortError"); },
|
|
780
|
+
});
|
|
781
|
+
try {
|
|
782
|
+
await requestJson("/repos/owner/repo", { signal: controller.signal });
|
|
783
|
+
} catch (error) { threw = error; }
|
|
784
|
+
if (threw?.name === "AbortError") console.log(" PASS 读体期间取消 → AbortError 上抛(不吞成 undefined)");
|
|
785
|
+
else { failed++; console.log(` FAIL 读体期间取消应抛 AbortError,实得 ${threw ? `${threw.name}: ${threw.message}` : "未抛(body 变 undefined)"}`); }
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// 2) 成功响应的读体超时 → 记一次失败,退避重试(重试轮换成成功)。
|
|
789
|
+
{
|
|
790
|
+
let attempts = 0;
|
|
791
|
+
globalThis.fetch = async () => fakeResponse({
|
|
792
|
+
status: 200,
|
|
793
|
+
json: async () => { attempts++; if (attempts === 1) throw makeError("TimeoutError"); return { ok: true }; },
|
|
794
|
+
});
|
|
795
|
+
const retried = await requestJson("/repos/owner/repo", {});
|
|
796
|
+
if (retried?.ok === true && attempts === 2) console.log(" PASS 读体超时(200)→ 记一次失败并重试成功");
|
|
797
|
+
else { failed++; console.log(` FAIL 读体超时(200)应重试一次,实得 attempts=${attempts} result=${JSON.stringify(retried)}`); }
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// 3) 4xx 的结论在读体之前就已确定,读体超时绝不重试——重试只会把
|
|
801
|
+
// 正确的 404 换成一句超时。一次调用、报 404 的报法。
|
|
802
|
+
{
|
|
803
|
+
let calls = 0;
|
|
804
|
+
globalThis.fetch = async () => { calls++; return fakeResponse({ status: 404, json: async () => { throw makeError("TimeoutError"); } }); };
|
|
805
|
+
let threw;
|
|
806
|
+
try {
|
|
807
|
+
await requestJson("/repos/owner/absent", {});
|
|
808
|
+
} catch (error) { threw = error; }
|
|
809
|
+
if (calls === 1 && /GitHub API 404/.test(threw?.message ?? "")) console.log(" PASS 读体超时(404)→ 不重试,保留 404 结论");
|
|
810
|
+
else { failed++; console.log(` FAIL 404+读体超时应一次调用报 404,实得 calls=${calls} error=${threw?.message}`); }
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 4) 403 限流同理:专用诊断靠响应头(不依赖 body),必须一次到位。
|
|
814
|
+
{
|
|
815
|
+
let calls = 0;
|
|
816
|
+
const limited = new Headers({ "x-ratelimit-remaining": "0", "x-ratelimit-reset": "4102444800" });
|
|
817
|
+
globalThis.fetch = async () => { calls++; return fakeResponse({ status: 403, headers: limited, json: async () => { throw makeError("TimeoutError"); } }); };
|
|
818
|
+
let threw;
|
|
819
|
+
try {
|
|
820
|
+
await requestJson("/search/repositories", {});
|
|
821
|
+
} catch (error) { threw = error; }
|
|
822
|
+
if (calls === 1 && /rate limit exceeded/.test(threw?.message ?? "")) console.log(" PASS 读体超时(403 限流)→ 不重试,保留限流诊断");
|
|
823
|
+
else { failed++; console.log(` FAIL 403+读体超时应一次调用给限流诊断,实得 calls=${calls} error=${threw?.message}`); }
|
|
824
|
+
}
|
|
825
|
+
} finally {
|
|
826
|
+
globalThis.fetch = realFetch;
|
|
827
|
+
}
|
|
828
|
+
if (failed > 0) process.exit(1);
|
|
829
|
+
}
|
|
690
830
|
if (process.argv.includes("--offline")) process.exit(0);
|
|
691
831
|
const apiBase = "https://api.github.com";
|
|
692
832
|
const result = await searchPlugins({ query: "", perPage: 3, apiBase });
|