dshmarket 1.66.23 → 1.66.24
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/client/client.js +25 -10
- package/lib/changelog.js +9 -1
- package/lib/types/changelog.d.ts +3 -2
- package/lib/verify.js +93 -4
- package/package.json +2 -2
- package/src/changelog.ts +12 -3
- package/src/client/MarketSection.tsx +29 -8
- package/src/client/locales.ts +4 -0
- package/src/verify.ts +92 -4
package/client/client.js
CHANGED
|
@@ -395,6 +395,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
395
395
|
notesNpm: "版本发布时间",
|
|
396
396
|
notesNone: "该插件未提供更新说明。",
|
|
397
397
|
notesSource: "市场还没收录这个插件的更新说明。",
|
|
398
|
+
notesSourceUnreadable: "这个插件不是从 GitHub 装的,市场读不到它的改动记录。",
|
|
398
399
|
notesSourceLink: "去仓库看改了什么",
|
|
399
400
|
notesSourceNoLink: "可以问问插件作者改了什么。",
|
|
400
401
|
notesLoadFail: "暂时读不到更新说明,稍后再试。",
|
|
@@ -589,6 +590,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
589
590
|
webdavRestore: "从 WebDAV 恢复",
|
|
590
591
|
autoBackup: "每天自动备份(打开市场时)",
|
|
591
592
|
webdavNote: "WebDAV 地址与用户名仅保存在当前浏览器;密码只存于服务端,每次会话需重新输入。",
|
|
593
|
+
webdavPathHint: "填完整的文件地址(含文件名):备份写进这个文件,恢复也从它读。开启自动备份后每天最多上传一次。",
|
|
592
594
|
localOnly: "WebDAV 地址和用户名只保存在此浏览器。",
|
|
593
595
|
credsWarning: "请注意:备份包含 profile 配置,以及可能含密钥的文件(config.toml、.env 等)。本地导出的文件会原样包含这些内容,上传 WebDAV 前请确认目标可信。",
|
|
594
596
|
gist: "GitHub Gist",
|
|
@@ -1112,6 +1114,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1112
1114
|
notesNpm: "Version publish times",
|
|
1113
1115
|
notesNone: "This plugin does not publish update notes.",
|
|
1114
1116
|
notesSource: "The market doesn't have update notes for this plugin yet.",
|
|
1117
|
+
notesSourceUnreadable: "This plugin was not installed from GitHub, so the market cannot read its change history.",
|
|
1115
1118
|
notesSourceLink: "See what changed in the repository",
|
|
1116
1119
|
notesSourceNoLink: "You can ask the plugin author what changed.",
|
|
1117
1120
|
notesLoadFail: "Update notes are unavailable right now — try again later.",
|
|
@@ -1306,6 +1309,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
1306
1309
|
webdavRestore: "Restore from WebDAV",
|
|
1307
1310
|
autoBackup: "Back up daily (when the market opens)",
|
|
1308
1311
|
webdavNote: "The WebDAV URL and username stay in this browser only; the password is kept server-side and must be re-entered each session.",
|
|
1312
|
+
webdavPathHint: "Give the whole file path, name included: the backup writes that file and restore reads it back. With automatic backup on it uploads at most once a day.",
|
|
1309
1313
|
localOnly: "The WebDAV URL and username stay in this browser only.",
|
|
1310
1314
|
credsWarning: "Note: backups include profile configuration and files that may hold secrets (config.toml, .env, …). Local exports are unmodified, so upload only to a WebDAV target you trust.",
|
|
1311
1315
|
gist: "GitHub Gist",
|
|
@@ -8591,6 +8595,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
8591
8595
|
const [restarting, setRestarting] = (0, react.useState)(false);
|
|
8592
8596
|
const [showTop, setShowTop] = (0, react.useState)(false);
|
|
8593
8597
|
const [backupBusy, setBackupBusy] = (0, react.useState)(false);
|
|
8598
|
+
const webdavInFlight = (0, react.useRef)(false);
|
|
8594
8599
|
const [backupMessage, setBackupMessage] = (0, react.useState)(null);
|
|
8595
8600
|
const [backupRestored, setBackupRestored] = (0, react.useState)(false);
|
|
8596
8601
|
const [pendingBackup, setPendingBackup] = (0, react.useState)(null);
|
|
@@ -10743,7 +10748,8 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10743
10748
|
}).catch((error) => setBackupMessage(String(error))).finally(() => setBackupBusy(false));
|
|
10744
10749
|
}, [finishRestore, pendingBackup]);
|
|
10745
10750
|
const runWebdav = (0, react.useCallback)((action) => {
|
|
10746
|
-
if (webdavUrl.trim() === "") return;
|
|
10751
|
+
if (webdavUrl.trim() === "" || webdavInFlight.current) return;
|
|
10752
|
+
webdavInFlight.current = true;
|
|
10747
10753
|
setBackupBusy(true);
|
|
10748
10754
|
setBackupMessage(null);
|
|
10749
10755
|
setRestoreErrors([]);
|
|
@@ -10766,7 +10772,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10766
10772
|
} catch {}
|
|
10767
10773
|
setBackupMessage(t("backupDone"));
|
|
10768
10774
|
}
|
|
10769
|
-
}).catch((error) => setBackupMessage(String(error))).finally(() =>
|
|
10775
|
+
}).catch((error) => setBackupMessage(String(error))).finally(() => {
|
|
10776
|
+
webdavInFlight.current = false;
|
|
10777
|
+
setBackupBusy(false);
|
|
10778
|
+
});
|
|
10770
10779
|
}, [
|
|
10771
10780
|
previewBackup,
|
|
10772
10781
|
t,
|
|
@@ -10890,6 +10899,10 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10890
10899
|
setExportIncludeConfig(false);
|
|
10891
10900
|
setExportOpen(true);
|
|
10892
10901
|
}, [installed, installedBundles]);
|
|
10902
|
+
const runWebdavRef = (0, react.useRef)(runWebdav);
|
|
10903
|
+
(0, react.useEffect)(() => {
|
|
10904
|
+
runWebdavRef.current = runWebdav;
|
|
10905
|
+
}, [runWebdav]);
|
|
10893
10906
|
(0, react.useEffect)(() => {
|
|
10894
10907
|
try {
|
|
10895
10908
|
localStorage.setItem(WEBDAV_STORAGE_KEY, JSON.stringify({
|
|
@@ -10898,18 +10911,19 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
10898
10911
|
auto: autoBackup
|
|
10899
10912
|
}));
|
|
10900
10913
|
} catch {}
|
|
10901
|
-
if (!autoBackup || webdavUrl.trim() === "") return;
|
|
10902
|
-
let last = 0;
|
|
10903
|
-
try {
|
|
10904
|
-
last = Number(localStorage.getItem("dshm-webdav-last")) || 0;
|
|
10905
|
-
} catch {}
|
|
10906
|
-
if (Date.now() - last >= 864e5) runWebdav("backup");
|
|
10907
10914
|
}, [
|
|
10908
10915
|
autoBackup,
|
|
10909
|
-
runWebdav,
|
|
10910
10916
|
webdavUrl,
|
|
10911
10917
|
webdavUser
|
|
10912
10918
|
]);
|
|
10919
|
+
(0, react.useEffect)(() => {
|
|
10920
|
+
if (!autoBackup) return;
|
|
10921
|
+
let last = 0;
|
|
10922
|
+
try {
|
|
10923
|
+
last = Number(localStorage.getItem("dshm-webdav-last")) || 0;
|
|
10924
|
+
} catch {}
|
|
10925
|
+
if (Date.now() - last >= 864e5) runWebdavRef.current("backup");
|
|
10926
|
+
}, [autoBackup]);
|
|
10913
10927
|
const sessionPendingRestart = doneUrls.length + restartNames.length + removedCount + toggleRestart + (backupRestored ? 1 : 0);
|
|
10914
10928
|
/**
|
|
10915
10929
|
* Plugins the HOST reports as restart-pending, independent of what this
|
|
@@ -12460,6 +12474,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
12460
12474
|
placeholder: t("webdavUrl"),
|
|
12461
12475
|
onChange: (e) => setWebdavUrl(e.target.value)
|
|
12462
12476
|
}),
|
|
12477
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("webdavPathHint") }),
|
|
12463
12478
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
12464
12479
|
className: Market_module_css_default.backupInput,
|
|
12465
12480
|
autoComplete: "username",
|
|
@@ -14283,7 +14298,7 @@ window.__ModuleLoader__.load({ id: "dshmarket", factory: (require) => {
|
|
|
14283
14298
|
})]
|
|
14284
14299
|
}) : updateNotes.kind === "source" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14285
14300
|
className: Market_module_css_default.spec,
|
|
14286
|
-
children: [t(updateNotes.reason === "unavailable" ? "notesLoadFail" : "notesSource"), typeof updateNotes.sourceUrl === "string" && /^https?:\/\//i.test(updateNotes.sourceUrl) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [lang === "zh" ? "" : " ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
14301
|
+
children: [t(updateNotes.reason === "unavailable" ? "notesLoadFail" : updateNotes.reason === "unreadable" ? "notesSourceUnreadable" : "notesSource"), typeof updateNotes.sourceUrl === "string" && /^https?:\/\//i.test(updateNotes.sourceUrl) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [lang === "zh" ? "" : " ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
14287
14302
|
className: Market_module_css_default.notesSourceLink,
|
|
14288
14303
|
href: updateNotes.sourceUrl,
|
|
14289
14304
|
target: "_blank",
|
package/lib/changelog.js
CHANGED
|
@@ -341,9 +341,17 @@ export async function updateNotesFor(profile, explicitDir, name) {
|
|
|
341
341
|
const specKey = repoKeyOf(spec) ?? lookupRepoFromUrl(spec);
|
|
342
342
|
// The market holds no notes for this install, but its history is still
|
|
343
343
|
// readable where it came from: say which, and point there.
|
|
344
|
+
//
|
|
345
|
+
// Notes come from the catalog's own release feed, which is built from
|
|
346
|
+
// GitHub, so a gitee or self-hosted git install has no feed to find:
|
|
347
|
+
// "the market has not collected this one yet" would promise something that
|
|
348
|
+
// cannot happen, and the reader waits for it (#819). Those say the source
|
|
349
|
+
// cannot be read instead. The url is the same either way — the repository
|
|
350
|
+
// is where the answer actually is.
|
|
351
|
+
const unreadableSource = githubIdentityOf(spec) === null && isGitHostedSpec(spec);
|
|
344
352
|
const fromSource = (reason) => ({
|
|
345
353
|
kind: 'source',
|
|
346
|
-
reason,
|
|
354
|
+
reason: unreadableSource ? 'unreadable' : reason,
|
|
347
355
|
sourceUrl: sourceWebUrl(spec) ?? specKey,
|
|
348
356
|
});
|
|
349
357
|
try {
|
package/lib/types/changelog.d.ts
CHANGED
|
@@ -106,9 +106,10 @@ export interface UpdateNotes {
|
|
|
106
106
|
/**
|
|
107
107
|
* Present for `source`: `missing` when the notes loaded without this
|
|
108
108
|
* plugin, `unavailable` when they could not be loaded at all — a passing
|
|
109
|
-
* failure, not a fact about the plugin
|
|
109
|
+
* failure, not a fact about the plugin — and `unreadable` when the source is
|
|
110
|
+
* one the market can never read, so waiting will not help (#819).
|
|
110
111
|
*/
|
|
111
|
-
reason?: 'missing' | 'unavailable';
|
|
112
|
+
reason?: 'missing' | 'unavailable' | 'unreadable';
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
115
|
* Resolve the notes for one installed plugin, or `{ kind: 'none' }`.
|
package/lib/verify.js
CHANGED
|
@@ -26,14 +26,15 @@
|
|
|
26
26
|
* or a declared entry artifact that is missing
|
|
27
27
|
* missing – not present in node_modules
|
|
28
28
|
*/
|
|
29
|
-
import { readFileSync } from 'node:fs';
|
|
29
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
30
|
+
import { createRequire, isBuiltin } from 'node:module';
|
|
30
31
|
import { Script } from 'node:vm';
|
|
31
32
|
import { join } from 'node:path';
|
|
32
33
|
import { listHotMounts, parseSimplePatch } from './hot.js';
|
|
33
34
|
import { userPatchPackageReferences } from './patch.js';
|
|
34
35
|
import { nameMatchesPackage } from './entry-identity.js';
|
|
35
36
|
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.js';
|
|
36
|
-
import { compareSemver, parsePatchFile, satisfiesRange } from './check.js';
|
|
37
|
+
import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.js';
|
|
37
38
|
import { dshHostInfo } from './dsh-install.js';
|
|
38
39
|
/**
|
|
39
40
|
* Whether every bundle patch the package declares parses to an empty list.
|
|
@@ -183,11 +184,15 @@ const isDshPeer = (peer) => peer === '@deepseek-ai/dsh' || peer.startsWith('@dee
|
|
|
183
184
|
* peers fail at once.
|
|
184
185
|
*/
|
|
185
186
|
export function hostPeerGate(activeProfileDir, name, facts) {
|
|
187
|
+
return peerGateAt(join(activeProfileDir, 'node_modules', name), name, facts);
|
|
188
|
+
}
|
|
189
|
+
/** Same prediction for a patch plugin resolved outside the profile's flat node_modules. */
|
|
190
|
+
function peerGateAt(packageDir, name, facts) {
|
|
186
191
|
if (facts.runtimeVersion === null)
|
|
187
192
|
return null;
|
|
188
193
|
let manifest;
|
|
189
194
|
try {
|
|
190
|
-
manifest = JSON.parse(readFileSync(join(
|
|
195
|
+
manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
|
191
196
|
}
|
|
192
197
|
catch {
|
|
193
198
|
return null;
|
|
@@ -215,6 +220,74 @@ export function hostPeerGate(activeProfileDir, name, facts) {
|
|
|
215
220
|
}
|
|
216
221
|
return null;
|
|
217
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* #867: only the bundle's own effective loader rows are evidence, never its
|
|
225
|
+
* ordinary dependency closure or arbitrary names in plugin configuration.
|
|
226
|
+
* Compose every declared file together so later disables take effect. An
|
|
227
|
+
* unreadable file or an expression-gated row gives no rejection evidence.
|
|
228
|
+
*/
|
|
229
|
+
function bundleRowPeerGate(packageDir, activeProfileDir, facts) {
|
|
230
|
+
if (facts.runtimeVersion === null)
|
|
231
|
+
return null;
|
|
232
|
+
try {
|
|
233
|
+
const patches = declaredBundlePatchFiles(packageDir).map(parsePatchFile);
|
|
234
|
+
if (patches.some(rows => rows === null))
|
|
235
|
+
return null;
|
|
236
|
+
const { resolvableRows } = composeLayers([{
|
|
237
|
+
label: 'bundle', kind: 'bundle', patches: patches.flatMap(rows => rows ?? []), parseError: null,
|
|
238
|
+
}]);
|
|
239
|
+
const checked = new Set();
|
|
240
|
+
for (const row of resolvableRows) {
|
|
241
|
+
if (row.activation !== 'required' || typeof row.name !== 'string')
|
|
242
|
+
continue;
|
|
243
|
+
// Bare npm names and subpath entries only. Relative files, builtins,
|
|
244
|
+
// aliases and expressions need runtime resolution; do not guess.
|
|
245
|
+
if (/^[.#/\\]|[:\\]/u.test(row.name) || isBuiltin(row.name))
|
|
246
|
+
continue;
|
|
247
|
+
const parts = row.name.split('/');
|
|
248
|
+
const name = parts.slice(0, row.name.startsWith('@') ? 2 : 1).join('/');
|
|
249
|
+
if (!/^(?:@[\w.-]+\/)?[\w-][\w.-]*$/u.test(name) || checked.has(name))
|
|
250
|
+
continue;
|
|
251
|
+
checked.add(name);
|
|
252
|
+
const directory = resolveBundleRowPackage(packageDir, activeProfileDir, name);
|
|
253
|
+
if (directory === null)
|
|
254
|
+
continue;
|
|
255
|
+
const gate = peerGateAt(directory, name, facts);
|
|
256
|
+
if (gate !== null)
|
|
257
|
+
return { name, gate };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch { /* malformed or cyclic patch data — no reliable prediction */ }
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Probe Node's search paths, without resolving an entry or requiring a
|
|
265
|
+
* package.json export. The real bundle path reaches pnpm's adjacent and
|
|
266
|
+
* nested dependencies; the profile anchor also covers workspace hoisting.
|
|
267
|
+
* Stop at a present directory even if its manifest is missing: a broken
|
|
268
|
+
* nearer copy must not borrow an incompatible manifest from a fallback.
|
|
269
|
+
*/
|
|
270
|
+
function resolveBundleRowPackage(packageDir, activeProfileDir, name) {
|
|
271
|
+
const bundleAnchor = join(realpathSync(packageDir), 'package.json');
|
|
272
|
+
const manifest = JSON.parse(readFileSync(bundleAnchor, 'utf8'));
|
|
273
|
+
// The host publishes dependencies carried by selected bundles into its
|
|
274
|
+
// profile resolution. A stray nested copy not declared by this bundle is
|
|
275
|
+
// no evidence of what a profile row will import.
|
|
276
|
+
const carried = manifest.name === name || Object.hasOwn(manifest.dependencies ?? {}, name)
|
|
277
|
+
|| Object.hasOwn(manifest.peerDependencies ?? {}, name);
|
|
278
|
+
const profileAnchor = join(activeProfileDir, 'package.json');
|
|
279
|
+
for (const anchor of carried ? [bundleAnchor, profileAnchor] : [profileAnchor]) {
|
|
280
|
+
for (const searchPath of createRequire(anchor).resolve.paths(name) ?? []) {
|
|
281
|
+
const candidate = join(searchPath, name);
|
|
282
|
+
try {
|
|
283
|
+
if (statSync(candidate).isDirectory())
|
|
284
|
+
return candidate;
|
|
285
|
+
}
|
|
286
|
+
catch { /* try the next Node search path */ }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
218
291
|
/** The way out of a peer gate, in the direction the versions allow (#758). */
|
|
219
292
|
export function peerGateRemedy(gate) {
|
|
220
293
|
if (gate.hostTooOld) {
|
|
@@ -391,7 +464,8 @@ export function verifyActivation(profile, name, live = new Set(listHotMounts()),
|
|
|
391
464
|
// range excludes the running runtime — predict that verdict here so
|
|
392
465
|
// the plugin reads as incompatible instead of promising a restart
|
|
393
466
|
// that cannot fix it.
|
|
394
|
-
const
|
|
467
|
+
const facts = hostFacts ?? defaultHostRuntimeFacts(activeProfileDir);
|
|
468
|
+
const gate = hostPeerGate(activeProfileDir, name, facts);
|
|
395
469
|
if (gate !== null) {
|
|
396
470
|
const remedy = peerGateRemedy(gate);
|
|
397
471
|
return {
|
|
@@ -403,6 +477,21 @@ export function verifyActivation(profile, name, live = new Set(listHotMounts()),
|
|
|
403
477
|
hot: false,
|
|
404
478
|
};
|
|
405
479
|
}
|
|
480
|
+
const child = bundleRowPeerGate(dir, activeProfileDir, facts);
|
|
481
|
+
if (child !== null) {
|
|
482
|
+
const { gate, name: plugin } = child;
|
|
483
|
+
const remedy = gate.hostTooOld
|
|
484
|
+
? { zh: '升级 dsh,或更新此插件包以匹配当前 dsh', en: 'upgrade dsh, or update this bundle to match the current dsh' }
|
|
485
|
+
: { zh: '更新此插件包,或安装与当前 dsh 兼容的版本;升级 dsh 解决不了', en: 'update this bundle or install a version compatible with the current dsh — upgrading dsh will not help' };
|
|
486
|
+
return {
|
|
487
|
+
state: 'incompatible',
|
|
488
|
+
reasons: [
|
|
489
|
+
`它加载的插件 ${plugin} 要求 ${gate.peer} ${gate.range},当前 dsh 为 ${gate.runtimeVersion},重启也不会生效;${remedy.zh} / its plugin ${plugin} requires ${gate.peer} ${gate.range}, but dsh runs ${gate.runtimeVersion} — a restart will not activate it; ${remedy.en}`,
|
|
490
|
+
],
|
|
491
|
+
bundle: true,
|
|
492
|
+
hot: false,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
406
495
|
const patch = patchTextOf(profile, name, activeProfileDir);
|
|
407
496
|
const complex = patch !== null && parseSimplePatch(patch) === null;
|
|
408
497
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dshmarket",
|
|
3
3
|
"description": "Visual plugin market inside DeepSeek Harness — browse, search, and one-click install community plugins. · DSH 可视化插件市场:逛一逛,点一下,装好。",
|
|
4
|
-
"version": "1.66.
|
|
4
|
+
"version": "1.66.24",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
44
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.2.0-rc.1",
|
|
44
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.7 || ^0.1.1-rc.2 || ^0.1.2-alpha.2 || ^0.2.0-rc.1 || ^0.2.1-alpha.3",
|
|
45
45
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
46
46
|
},
|
|
47
47
|
"dsh": {
|
package/src/changelog.ts
CHANGED
|
@@ -351,9 +351,10 @@ export interface UpdateNotes {
|
|
|
351
351
|
/**
|
|
352
352
|
* Present for `source`: `missing` when the notes loaded without this
|
|
353
353
|
* plugin, `unavailable` when they could not be loaded at all — a passing
|
|
354
|
-
* failure, not a fact about the plugin
|
|
354
|
+
* failure, not a fact about the plugin — and `unreadable` when the source is
|
|
355
|
+
* one the market can never read, so waiting will not help (#819).
|
|
355
356
|
*/
|
|
356
|
-
reason?: 'missing' | 'unavailable'
|
|
357
|
+
reason?: 'missing' | 'unavailable' | 'unreadable'
|
|
357
358
|
}
|
|
358
359
|
|
|
359
360
|
/**
|
|
@@ -380,9 +381,17 @@ export async function updateNotesFor(
|
|
|
380
381
|
const specKey = repoKeyOf(spec) ?? lookupRepoFromUrl(spec)
|
|
381
382
|
// The market holds no notes for this install, but its history is still
|
|
382
383
|
// readable where it came from: say which, and point there.
|
|
384
|
+
//
|
|
385
|
+
// Notes come from the catalog's own release feed, which is built from
|
|
386
|
+
// GitHub, so a gitee or self-hosted git install has no feed to find:
|
|
387
|
+
// "the market has not collected this one yet" would promise something that
|
|
388
|
+
// cannot happen, and the reader waits for it (#819). Those say the source
|
|
389
|
+
// cannot be read instead. The url is the same either way — the repository
|
|
390
|
+
// is where the answer actually is.
|
|
391
|
+
const unreadableSource = githubIdentityOf(spec) === null && isGitHostedSpec(spec)
|
|
383
392
|
const fromSource = (reason: 'missing' | 'unavailable'): UpdateNotes => ({
|
|
384
393
|
kind: 'source',
|
|
385
|
-
reason,
|
|
394
|
+
reason: unreadableSource ? 'unreadable' : reason,
|
|
386
395
|
sourceUrl: sourceWebUrl(spec) ?? specKey,
|
|
387
396
|
})
|
|
388
397
|
try {
|
|
@@ -1996,13 +1996,13 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
1996
1996
|
commits?: { items: NoteCommit[]; found: boolean }
|
|
1997
1997
|
npmTimes?: Array<{ version: string; date: string }>
|
|
1998
1998
|
sourceUrl?: string | null
|
|
1999
|
-
reason?: 'missing' | 'unavailable'
|
|
1999
|
+
reason?: 'missing' | 'unavailable' | 'unreadable'
|
|
2000
2000
|
}
|
|
2001
2001
|
type ResolvedNotes =
|
|
2002
2002
|
| { kind: 'release'; release: NoteRelease }
|
|
2003
2003
|
| { kind: 'commits'; commits: { items: NoteCommit[]; found: boolean } }
|
|
2004
2004
|
| { kind: 'npm'; npmTimes: Array<{ version: string; date: string }> }
|
|
2005
|
-
| { kind: 'source'; sourceUrl?: string | null; reason?: 'missing' | 'unavailable' }
|
|
2005
|
+
| { kind: 'source'; sourceUrl?: string | null; reason?: 'missing' | 'unavailable' | 'unreadable' }
|
|
2006
2006
|
| { kind: 'none' }
|
|
2007
2007
|
interface CompatibilityNotice {
|
|
2008
2008
|
code: 'soft-incompatible'
|
|
@@ -2279,6 +2279,7 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
2279
2279
|
const [restarting, setRestarting] = useState(false)
|
|
2280
2280
|
const [showTop, setShowTop] = useState(false)
|
|
2281
2281
|
const [backupBusy, setBackupBusy] = useState(false)
|
|
2282
|
+
const webdavInFlight = useRef(false)
|
|
2282
2283
|
const [backupMessage, setBackupMessage] = useState<string | null>(null)
|
|
2283
2284
|
const [backupRestored, setBackupRestored] = useState(false)
|
|
2284
2285
|
const [pendingBackup, setPendingBackup] = useState<unknown>(null)
|
|
@@ -4576,7 +4577,8 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4576
4577
|
}, [finishRestore, pendingBackup])
|
|
4577
4578
|
|
|
4578
4579
|
const runWebdav = useCallback((action: 'backup' | 'restore') => {
|
|
4579
|
-
if (webdavUrl.trim() === '') return
|
|
4580
|
+
if (webdavUrl.trim() === '' || webdavInFlight.current) return
|
|
4581
|
+
webdavInFlight.current = true
|
|
4580
4582
|
setBackupBusy(true)
|
|
4581
4583
|
setBackupMessage(null)
|
|
4582
4584
|
setRestoreErrors([])
|
|
@@ -4593,7 +4595,10 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4593
4595
|
try { localStorage.setItem('dshm-webdav-last', String(Date.now())) } catch { /* storage unavailable */ }
|
|
4594
4596
|
setBackupMessage(t('backupDone'))
|
|
4595
4597
|
}
|
|
4596
|
-
}).catch(error => setBackupMessage(String(error))).finally(() =>
|
|
4598
|
+
}).catch(error => setBackupMessage(String(error))).finally(() => {
|
|
4599
|
+
webdavInFlight.current = false
|
|
4600
|
+
setBackupBusy(false)
|
|
4601
|
+
})
|
|
4597
4602
|
}, [previewBackup, t, webdavPassword, webdavUrl, webdavUser])
|
|
4598
4603
|
|
|
4599
4604
|
/** Map the server's token-source string to a localized label. */
|
|
@@ -4718,6 +4723,9 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4718
4723
|
setExportOpen(true)
|
|
4719
4724
|
}, [installed, installedBundles])
|
|
4720
4725
|
|
|
4726
|
+
const runWebdavRef = useRef(runWebdav)
|
|
4727
|
+
useEffect(() => { runWebdavRef.current = runWebdav }, [runWebdav])
|
|
4728
|
+
|
|
4721
4729
|
useEffect(() => {
|
|
4722
4730
|
// Persist only the non-secret WebDAV settings; the password stays
|
|
4723
4731
|
// server-side/in-memory (see savedWebdav). Storage itself may be
|
|
@@ -4725,13 +4733,18 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
4725
4733
|
try {
|
|
4726
4734
|
localStorage.setItem(WEBDAV_STORAGE_KEY, JSON.stringify({ url: webdavUrl, username: webdavUser, auto: autoBackup }))
|
|
4727
4735
|
} catch { /* storage unavailable — config just won't survive reload */ }
|
|
4728
|
-
|
|
4736
|
+
}, [autoBackup, webdavUrl, webdavUser])
|
|
4737
|
+
|
|
4738
|
+
// Check when the market opens or daily backup is enabled. Editing the
|
|
4739
|
+
// settings (or changing the locale) must not start another upload.
|
|
4740
|
+
useEffect(() => {
|
|
4741
|
+
if (!autoBackup) return
|
|
4729
4742
|
let last = 0
|
|
4730
4743
|
try {
|
|
4731
4744
|
last = Number(localStorage.getItem('dshm-webdav-last')) || 0
|
|
4732
4745
|
} catch { /* ignore */ }
|
|
4733
|
-
if (Date.now() - last >= 24 * 60 * 60 * 1000)
|
|
4734
|
-
}, [autoBackup
|
|
4746
|
+
if (Date.now() - last >= 24 * 60 * 60 * 1000) runWebdavRef.current('backup')
|
|
4747
|
+
}, [autoBackup])
|
|
4735
4748
|
|
|
4736
4749
|
// #558: count restart-pending session changes (restartNames), not every
|
|
4737
4750
|
// completed `updatedNames` entry (which would double-count a batch update
|
|
@@ -6067,6 +6080,12 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
6067
6080
|
]}
|
|
6068
6081
|
/>
|
|
6069
6082
|
<Input className={css.backupInput} icon={<IconLinkOutline14 size={14} />} type="url" value={webdavUrl} placeholder={t('webdavUrl')} onChange={e => setWebdavUrl(e.target.value)} />
|
|
6083
|
+
{/* The field had a placeholder and nothing else, so "is this a
|
|
6084
|
+
folder or a file?" was answered only by trying it — and a
|
|
6085
|
+
reporter who read it as a folder expected the market to
|
|
6086
|
+
name the file (#868). Name the shape, and say why the
|
|
6087
|
+
name has to stay put: restore looks for that same one. */}
|
|
6088
|
+
<p>{t('webdavPathHint')}</p>
|
|
6070
6089
|
<Input className={css.backupInput} autoComplete="username" value={webdavUser} placeholder={t('webdavUser')} onChange={e => setWebdavUser(e.target.value)} />
|
|
6071
6090
|
<Input className={css.backupInput} type="password" autoComplete="current-password" value={webdavPassword} placeholder={t('webdavPassword')} onChange={e => setWebdavPassword(e.target.value)} />
|
|
6072
6091
|
<div className={css.backupActions}>
|
|
@@ -7631,7 +7650,9 @@ export function MarketSection(props: MarketSectionProps) {
|
|
|
7631
7650
|
)
|
|
7632
7651
|
: updateNotes.kind === 'source' ? (
|
|
7633
7652
|
<div className={css.spec}>
|
|
7634
|
-
{t(updateNotes.reason === 'unavailable'
|
|
7653
|
+
{t(updateNotes.reason === 'unavailable'
|
|
7654
|
+
? 'notesLoadFail'
|
|
7655
|
+
: updateNotes.reason === 'unreadable' ? 'notesSourceUnreadable' : 'notesSource')}
|
|
7635
7656
|
{typeof updateNotes.sourceUrl === 'string' && /^https?:\/\//i.test(updateNotes.sourceUrl)
|
|
7636
7657
|
? <>{lang === 'zh' ? '' : ' '}<a className={css.notesSourceLink} href={updateNotes.sourceUrl} target="_blank" rel="noreferrer">{t('notesSourceLink')}</a></>
|
|
7637
7658
|
: updateNotes.reason !== 'unavailable' && <>{lang === 'zh' ? '' : ' '}{t('notesSourceNoLink')}</>}
|
package/src/client/locales.ts
CHANGED
|
@@ -198,6 +198,7 @@ export const zh = {
|
|
|
198
198
|
notesNpm: '版本发布时间',
|
|
199
199
|
notesNone: '该插件未提供更新说明。',
|
|
200
200
|
notesSource: '市场还没收录这个插件的更新说明。',
|
|
201
|
+
notesSourceUnreadable: '这个插件不是从 GitHub 装的,市场读不到它的改动记录。',
|
|
201
202
|
notesSourceLink: '去仓库看改了什么',
|
|
202
203
|
notesSourceNoLink: '可以问问插件作者改了什么。',
|
|
203
204
|
notesLoadFail: '暂时读不到更新说明,稍后再试。',
|
|
@@ -398,6 +399,7 @@ export const zh = {
|
|
|
398
399
|
webdavRestore: '从 WebDAV 恢复',
|
|
399
400
|
autoBackup: '每天自动备份(打开市场时)',
|
|
400
401
|
webdavNote: 'WebDAV 地址与用户名仅保存在当前浏览器;密码只存于服务端,每次会话需重新输入。',
|
|
402
|
+
webdavPathHint: '填完整的文件地址(含文件名):备份写进这个文件,恢复也从它读。开启自动备份后每天最多上传一次。',
|
|
401
403
|
localOnly: 'WebDAV 地址和用户名只保存在此浏览器。',
|
|
402
404
|
credsWarning: '请注意:备份包含 profile 配置,以及可能含密钥的文件(config.toml、.env 等)。本地导出的文件会原样包含这些内容,上传 WebDAV 前请确认目标可信。',
|
|
403
405
|
gist: 'GitHub Gist',
|
|
@@ -935,6 +937,7 @@ export const en: Record<MarketKey, string> = {
|
|
|
935
937
|
notesNpm: 'Version publish times',
|
|
936
938
|
notesNone: 'This plugin does not publish update notes.',
|
|
937
939
|
notesSource: "The market doesn't have update notes for this plugin yet.",
|
|
940
|
+
notesSourceUnreadable: 'This plugin was not installed from GitHub, so the market cannot read its change history.',
|
|
938
941
|
notesSourceLink: 'See what changed in the repository',
|
|
939
942
|
notesSourceNoLink: 'You can ask the plugin author what changed.',
|
|
940
943
|
notesLoadFail: 'Update notes are unavailable right now — try again later.',
|
|
@@ -1129,6 +1132,7 @@ export const en: Record<MarketKey, string> = {
|
|
|
1129
1132
|
webdavRestore: 'Restore from WebDAV',
|
|
1130
1133
|
autoBackup: 'Back up daily (when the market opens)',
|
|
1131
1134
|
webdavNote: 'The WebDAV URL and username stay in this browser only; the password is kept server-side and must be re-entered each session.',
|
|
1135
|
+
webdavPathHint: 'Give the whole file path, name included: the backup writes that file and restore reads it back. With automatic backup on it uploads at most once a day.',
|
|
1132
1136
|
localOnly: 'The WebDAV URL and username stay in this browser only.',
|
|
1133
1137
|
credsWarning: 'Note: backups include profile configuration and files that may hold secrets (config.toml, .env, …). Local exports are unmodified, so upload only to a WebDAV target you trust.',
|
|
1134
1138
|
gist: 'GitHub Gist',
|
package/src/verify.ts
CHANGED
|
@@ -27,14 +27,15 @@
|
|
|
27
27
|
* missing – not present in node_modules
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { readFileSync } from 'node:fs'
|
|
30
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs'
|
|
31
|
+
import { createRequire, isBuiltin } from 'node:module'
|
|
31
32
|
import { Script } from 'node:vm'
|
|
32
33
|
import { join } from 'node:path'
|
|
33
34
|
import { listHotMounts, parseSimplePatch } from './hot.ts'
|
|
34
35
|
import { userPatchPackageReferences } from './patch.ts'
|
|
35
36
|
import { nameMatchesPackage } from './entry-identity.ts'
|
|
36
37
|
import { bundlePatchInsertedIds, declaredBundlePatchFiles, hasDshManifest, hasLoadableEntry, profileDir, readInstalled } from './profile.ts'
|
|
37
|
-
import { compareSemver, parsePatchFile, satisfiesRange } from './check.ts'
|
|
38
|
+
import { compareSemver, composeLayers, parsePatchFile, satisfiesRange } from './check.ts'
|
|
38
39
|
import { dshHostInfo } from './dsh-install.ts'
|
|
39
40
|
|
|
40
41
|
export type ActivationState = 'live' | 'restart' | 'incompatible' | 'inert' | 'broken' | 'missing' | 'disabled'
|
|
@@ -247,11 +248,16 @@ const isDshPeer = (peer: string): boolean => peer === '@deepseek-ai/dsh' || peer
|
|
|
247
248
|
* peers fail at once.
|
|
248
249
|
*/
|
|
249
250
|
export function hostPeerGate(activeProfileDir: string, name: string, facts: HostRuntimeFacts): PeerGate | null {
|
|
251
|
+
return peerGateAt(join(activeProfileDir, 'node_modules', name), name, facts)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Same prediction for a patch plugin resolved outside the profile's flat node_modules. */
|
|
255
|
+
function peerGateAt(packageDir: string, name: string, facts: HostRuntimeFacts): PeerGate | null {
|
|
250
256
|
if (facts.runtimeVersion === null) return null
|
|
251
257
|
let manifest: { version?: unknown; peerDependencies?: Record<string, unknown> }
|
|
252
258
|
try {
|
|
253
259
|
manifest = JSON.parse(
|
|
254
|
-
readFileSync(join(
|
|
260
|
+
readFileSync(join(packageDir, 'package.json'), 'utf8'),
|
|
255
261
|
)
|
|
256
262
|
} catch {
|
|
257
263
|
return null
|
|
@@ -278,6 +284,72 @@ export function hostPeerGate(activeProfileDir: string, name: string, facts: Host
|
|
|
278
284
|
return null
|
|
279
285
|
}
|
|
280
286
|
|
|
287
|
+
/**
|
|
288
|
+
* #867: only the bundle's own effective loader rows are evidence, never its
|
|
289
|
+
* ordinary dependency closure or arbitrary names in plugin configuration.
|
|
290
|
+
* Compose every declared file together so later disables take effect. An
|
|
291
|
+
* unreadable file or an expression-gated row gives no rejection evidence.
|
|
292
|
+
*/
|
|
293
|
+
function bundleRowPeerGate(
|
|
294
|
+
packageDir: string,
|
|
295
|
+
activeProfileDir: string,
|
|
296
|
+
facts: HostRuntimeFacts,
|
|
297
|
+
): { name: string; gate: PeerGate } | null {
|
|
298
|
+
if (facts.runtimeVersion === null) return null
|
|
299
|
+
try {
|
|
300
|
+
const patches = declaredBundlePatchFiles(packageDir).map(parsePatchFile)
|
|
301
|
+
if (patches.some(rows => rows === null)) return null
|
|
302
|
+
const { resolvableRows } = composeLayers([{
|
|
303
|
+
label: 'bundle', kind: 'bundle', patches: patches.flatMap(rows => rows ?? []), parseError: null,
|
|
304
|
+
}])
|
|
305
|
+
const checked = new Set<string>()
|
|
306
|
+
for (const row of resolvableRows) {
|
|
307
|
+
if (row.activation !== 'required' || typeof row.name !== 'string') continue
|
|
308
|
+
// Bare npm names and subpath entries only. Relative files, builtins,
|
|
309
|
+
// aliases and expressions need runtime resolution; do not guess.
|
|
310
|
+
if (/^[.#/\\]|[:\\]/u.test(row.name) || isBuiltin(row.name)) continue
|
|
311
|
+
const parts = row.name.split('/')
|
|
312
|
+
const name = parts.slice(0, row.name.startsWith('@') ? 2 : 1).join('/')
|
|
313
|
+
if (!/^(?:@[\w.-]+\/)?[\w-][\w.-]*$/u.test(name) || checked.has(name)) continue
|
|
314
|
+
checked.add(name)
|
|
315
|
+
const directory = resolveBundleRowPackage(packageDir, activeProfileDir, name)
|
|
316
|
+
if (directory === null) continue
|
|
317
|
+
const gate = peerGateAt(directory, name, facts)
|
|
318
|
+
if (gate !== null) return { name, gate }
|
|
319
|
+
}
|
|
320
|
+
} catch { /* malformed or cyclic patch data — no reliable prediction */ }
|
|
321
|
+
return null
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Probe Node's search paths, without resolving an entry or requiring a
|
|
326
|
+
* package.json export. The real bundle path reaches pnpm's adjacent and
|
|
327
|
+
* nested dependencies; the profile anchor also covers workspace hoisting.
|
|
328
|
+
* Stop at a present directory even if its manifest is missing: a broken
|
|
329
|
+
* nearer copy must not borrow an incompatible manifest from a fallback.
|
|
330
|
+
*/
|
|
331
|
+
function resolveBundleRowPackage(packageDir: string, activeProfileDir: string, name: string): string | null {
|
|
332
|
+
const bundleAnchor = join(realpathSync(packageDir), 'package.json')
|
|
333
|
+
const manifest = JSON.parse(readFileSync(bundleAnchor, 'utf8')) as {
|
|
334
|
+
name?: string; dependencies?: Record<string, unknown>; peerDependencies?: Record<string, unknown>
|
|
335
|
+
}
|
|
336
|
+
// The host publishes dependencies carried by selected bundles into its
|
|
337
|
+
// profile resolution. A stray nested copy not declared by this bundle is
|
|
338
|
+
// no evidence of what a profile row will import.
|
|
339
|
+
const carried = manifest.name === name || Object.hasOwn(manifest.dependencies ?? {}, name)
|
|
340
|
+
|| Object.hasOwn(manifest.peerDependencies ?? {}, name)
|
|
341
|
+
const profileAnchor = join(activeProfileDir, 'package.json')
|
|
342
|
+
for (const anchor of carried ? [bundleAnchor, profileAnchor] : [profileAnchor]) {
|
|
343
|
+
for (const searchPath of createRequire(anchor).resolve.paths(name) ?? []) {
|
|
344
|
+
const candidate = join(searchPath, name)
|
|
345
|
+
try {
|
|
346
|
+
if (statSync(candidate).isDirectory()) return candidate
|
|
347
|
+
} catch { /* try the next Node search path */ }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
|
|
281
353
|
/** The way out of a peer gate, in the direction the versions allow (#758). */
|
|
282
354
|
export function peerGateRemedy(gate: PeerGate): { zh: string; en: string } {
|
|
283
355
|
if (gate.hostTooOld) {
|
|
@@ -467,7 +539,8 @@ export function verifyActivation(
|
|
|
467
539
|
// range excludes the running runtime — predict that verdict here so
|
|
468
540
|
// the plugin reads as incompatible instead of promising a restart
|
|
469
541
|
// that cannot fix it.
|
|
470
|
-
const
|
|
542
|
+
const facts = hostFacts ?? defaultHostRuntimeFacts(activeProfileDir)
|
|
543
|
+
const gate = hostPeerGate(activeProfileDir, name, facts)
|
|
471
544
|
if (gate !== null) {
|
|
472
545
|
const remedy = peerGateRemedy(gate)
|
|
473
546
|
return {
|
|
@@ -479,6 +552,21 @@ export function verifyActivation(
|
|
|
479
552
|
hot: false,
|
|
480
553
|
}
|
|
481
554
|
}
|
|
555
|
+
const child = bundleRowPeerGate(dir, activeProfileDir, facts)
|
|
556
|
+
if (child !== null) {
|
|
557
|
+
const { gate, name: plugin } = child
|
|
558
|
+
const remedy = gate.hostTooOld
|
|
559
|
+
? { zh: '升级 dsh,或更新此插件包以匹配当前 dsh', en: 'upgrade dsh, or update this bundle to match the current dsh' }
|
|
560
|
+
: { zh: '更新此插件包,或安装与当前 dsh 兼容的版本;升级 dsh 解决不了', en: 'update this bundle or install a version compatible with the current dsh — upgrading dsh will not help' }
|
|
561
|
+
return {
|
|
562
|
+
state: 'incompatible',
|
|
563
|
+
reasons: [
|
|
564
|
+
`它加载的插件 ${plugin} 要求 ${gate.peer} ${gate.range},当前 dsh 为 ${gate.runtimeVersion},重启也不会生效;${remedy.zh} / its plugin ${plugin} requires ${gate.peer} ${gate.range}, but dsh runs ${gate.runtimeVersion} — a restart will not activate it; ${remedy.en}`,
|
|
565
|
+
],
|
|
566
|
+
bundle: true,
|
|
567
|
+
hot: false,
|
|
568
|
+
}
|
|
569
|
+
}
|
|
482
570
|
const patch = patchTextOf(profile, name, activeProfileDir)
|
|
483
571
|
const complex = patch !== null && parseSimplePatch(patch) === null
|
|
484
572
|
return {
|