dsh-m 0.9.76 → 0.9.77

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.
@@ -1,5 +1,13 @@
1
1
  import { fetchTextLimited } from './httpx.js';
2
2
  const MANIFEST_HOST = 'raw.githubusercontent.com';
3
+ /**
4
+ * 0.9.77 兜底腿(桌面机 raw 被墙实录):raw.githubusercontent.com 直连在部分网络下
5
+ * TLS 握手即被 reset,而 registry 清单自身早有 default-raw → default-jsdelivr 降级链、
6
+ * 社区目录有 jsdelivr/npmmirror/unpkg 三路由——截图 manifest 是全市场唯一单源抓取点,
7
+ * 症状为「列表/README/安装全正常,唯独精选详情无截图」。此处补齐同款第二腿:
8
+ * jsDelivr 的 GitHub 镜像,仅当 raw 抓取层失败(含 404/超时/host guard 拒绝)时启用。
9
+ */
10
+ const MIRROR_HOST = 'cdn.jsdelivr.net';
3
11
  const MANIFEST_FILE = 'screenshots.json';
4
12
  const MAX_MANIFEST_BYTES = 64 * 1024;
5
13
  const MAX_SCREENSHOTS = 8;
@@ -9,7 +17,8 @@ const OWNER_MAX = 39;
9
17
  const REPO_MAX = 100;
10
18
  const GITHUB_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/;
11
19
  const CONTROL_RE = /[\u0000-\u001f\u007f]/;
12
- function manifestUrl(github) {
20
+ /** owner/repo 校验与归一(GITHUB_REPO_RE 与 registry 同款);非法输入返回 null(零抓取)。 */
21
+ function repoSlug(github) {
13
22
  const value = typeof github === 'string' ? github.trim() : '';
14
23
  if (!GITHUB_REPO_RE.test(value))
15
24
  return null;
@@ -18,9 +27,25 @@ function manifestUrl(github) {
18
27
  const repo = value.slice(slash + 1);
19
28
  if (owner.length > OWNER_MAX || repo.length > REPO_MAX || repo === '.' || repo === '..')
20
29
  return null;
21
- return `https://${MANIFEST_HOST}/${owner}/${repo}/HEAD/${MANIFEST_FILE}`;
30
+ return `${owner}/${repo}`;
31
+ }
32
+ /**
33
+ * raw 规范形(/HEAD/ = 默认分支)。它同时是 parseManifest 的路径解析基址:无论哪条腿供数,
34
+ * 返回的图片 URL 一律落在 *.githubusercontent.com——恰好通过客户端 safeScreenshots 图床白名单
35
+ * (main.jsx 只放行 github.com / *.githubusercontent.com),镜像腿若原样回 jsdelivr URL 会被
36
+ * 客户端整体过滤、白修。
37
+ */
38
+ function rawManifestUrl(slug) {
39
+ return `https://${MANIFEST_HOST}/${slug}/HEAD/${MANIFEST_FILE}`;
22
40
  }
23
- function screenshotUrl(manifest, rawPath) {
41
+ /**
42
+ * jsDelivr 镜像形(@HEAD 与 raw /HEAD/ 同义,默认分支,实测可用)。镜像存在 CDN 缓存滞后
43
+ * (registry default-jsdelivr 同款取舍,可用 purge.jsdelivr.net 手动清缓存),截图属低频变更元数据,可接受。
44
+ */
45
+ function mirrorManifestUrl(slug) {
46
+ return `https://${MIRROR_HOST}/gh/${slug}@HEAD/${MANIFEST_FILE}`;
47
+ }
48
+ function screenshotUrl(baseUrl, rawPath) {
24
49
  if (typeof rawPath !== 'string')
25
50
  return null;
26
51
  const path = rawPath.trim();
@@ -35,10 +60,10 @@ function screenshotUrl(manifest, rawPath) {
35
60
  const segments = path.split('/');
36
61
  if (segments.some((part) => part === '' || part === '.' || part === '..'))
37
62
  return null;
38
- const base = new URL('.', manifest);
63
+ const base = new URL('.', baseUrl);
39
64
  return new URL(segments.map((part) => encodeURIComponent(part)).join('/'), base).toString();
40
65
  }
41
- function parseManifest(text, manifestUrl) {
66
+ function parseManifest(text, baseUrl) {
42
67
  let parsed;
43
68
  try {
44
69
  parsed = JSON.parse(text);
@@ -50,14 +75,14 @@ function parseManifest(text, manifestUrl) {
50
75
  return [];
51
76
  const result = [];
52
77
  for (const path of parsed) {
53
- const url = screenshotUrl(manifestUrl, path);
78
+ const url = screenshotUrl(baseUrl, path);
54
79
  if (!url)
55
80
  return [];
56
81
  result.push(url);
57
82
  }
58
83
  return result;
59
84
  }
60
- function assertManifestHost(raw, isRedirectHop) {
85
+ function assertManifestHost(raw, isRedirectHop, allowedHost) {
61
86
  let url;
62
87
  try {
63
88
  url = new URL(raw);
@@ -65,14 +90,31 @@ function assertManifestHost(raw, isRedirectHop) {
65
90
  catch {
66
91
  throw new Error(isRedirectHop ? 'screenshot manifest redirect URL is invalid' : 'invalid screenshot manifest request URL');
67
92
  }
68
- if (url.protocol !== 'https:' || url.hostname !== MANIFEST_HOST || url.username || url.password || url.port) {
93
+ if (url.protocol !== 'https:' || url.hostname !== allowedHost || url.username || url.password || url.port) {
69
94
  throw new Error(isRedirectHop
70
- ? 'screenshot manifest redirects must remain on raw.githubusercontent.com'
71
- : 'screenshot manifest request URL must be https://raw.githubusercontent.com');
95
+ ? `screenshot manifest redirects must remain on ${allowedHost}`
96
+ : `screenshot manifest request URL must be https://${allowedHost}`);
72
97
  }
73
98
  }
99
+ /** 单腿抓取:每腿独立逐跳 host guard(初始跳 + 重定向都不许离开本腿源),5s / 64KB 上限逐腿独立生效。 */
100
+ async function fetchManifestLeg(fetchText, url, allowedHost) {
101
+ let hop = 0;
102
+ return fetchText(url, {
103
+ timeoutMs: 5_000,
104
+ maxBytes: MAX_MANIFEST_BYTES,
105
+ headers: { accept: 'application/json' },
106
+ onRequest: (hopUrl) => {
107
+ // 逐跳 host guard;错误文案区分初始跳与重定向跳,避免调试误导(评审建议落盘)。
108
+ const isRedirectHop = hop > 0;
109
+ hop += 1;
110
+ assertManifestHost(hopUrl, isRedirectHop, allowedHost);
111
+ },
112
+ });
113
+ }
74
114
  /**
75
115
  * Read an author-owned root screenshots.json without consulting Community Catalog.
116
+ * 两腿链:raw 抓取层失败 → jsDelivr 镜像;两腿全败 → 空图库。解析层的「空」是权威结果
117
+ * (两腿读的是同一 repo 文件,镜像不会有不同内容),不再触发兜底。
76
118
  * All network access is bounded, and a rejected/empty manifest degrades to no gallery.
77
119
  */
78
120
  export function createPrimaryScreenshotReader(options = {}) {
@@ -81,10 +123,11 @@ export function createPrimaryScreenshotReader(options = {}) {
81
123
  const cache = new Map();
82
124
  const flights = new Map();
83
125
  return async (github) => {
84
- const url = manifestUrl(github);
85
- if (!url)
126
+ const slug = repoSlug(github);
127
+ if (!slug)
86
128
  return [];
87
- const key = url.toLowerCase();
129
+ const rawUrl = rawManifestUrl(slug);
130
+ const key = rawUrl.toLowerCase();
88
131
  const timestamp = now();
89
132
  for (const [cachedKey, value] of cache) {
90
133
  if (value.expiresAt <= timestamp)
@@ -98,24 +141,20 @@ export function createPrimaryScreenshotReader(options = {}) {
98
141
  return [...(await pending)];
99
142
  const flight = (async () => {
100
143
  let screenshots = [];
101
- let hop = 0;
102
144
  try {
103
- const text = await fetchText(url, {
104
- timeoutMs: 5_000,
105
- maxBytes: MAX_MANIFEST_BYTES,
106
- headers: { accept: 'application/json' },
107
- onRequest: (hopUrl) => {
108
- // 逐跳 host guard;错误文案区分初始跳与重定向跳,避免调试误导(评审建议落盘)。
109
- const isRedirectHop = hop > 0;
110
- hop += 1;
111
- assertManifestHost(hopUrl, isRedirectHop);
112
- },
113
- });
114
- screenshots = parseManifest(text, url);
145
+ const text = await fetchManifestLeg(fetchText, rawUrl, MANIFEST_HOST);
146
+ screenshots = parseManifest(text, rawUrl);
115
147
  }
116
148
  catch {
117
- // Images are optional metadata: a missing/unavailable manifest must not block detail/install.
118
- screenshots = [];
149
+ try {
150
+ const mirrorUrl = mirrorManifestUrl(slug);
151
+ const text = await fetchManifestLeg(fetchText, mirrorUrl, MIRROR_HOST);
152
+ screenshots = parseManifest(text, rawUrl);
153
+ }
154
+ catch {
155
+ // Images are optional metadata: a missing/unavailable manifest must not block detail/install.
156
+ screenshots = [];
157
+ }
119
158
  }
120
159
  cache.set(key, {
121
160
  screenshots,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.76",
3
+ "version": "0.9.77",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",