@max-null/dsh-plugin-center 0.2.1 → 0.2.3
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.js +8 -94
- package/dist/engine.d.ts +12 -5
- package/dist/engine.js +103 -21
- package/dist/market.js +16 -2
- package/dist/reconcile.d.ts +12 -0
- package/dist/reconcile.js +81 -0
- package/dist/rpc.js +1 -1
- package/dist/toggle.d.ts +1 -1
- package/dist/toggle.js +15 -1
- package/package.json +3 -3
package/client.js
CHANGED
|
@@ -413,11 +413,6 @@ var STRINGS = {
|
|
|
413
413
|
diagInstalled: "\u5DF2\u5B89\u88C5\u63D2\u4EF6\uFF08{n}\uFF09",
|
|
414
414
|
diagDisabled: "\u7981\u7528\u72B6\u6001",
|
|
415
415
|
diagPnpmLog: "pnpm \u65E5\u5FD7\uFF08\u5C3E\u90E8\uFF09",
|
|
416
|
-
aiTitle: "AI \u63A8\u8350",
|
|
417
|
-
aiPlaceholder: "\u63CF\u8FF0\u4F60\u7684\u9700\u6C42\uFF0C\u8BA9 AI \u63A8\u8350\u63D2\u4EF6\u2026\uFF08\u4F8B\u5982\uFF1A\u80FD\u9884\u89C8 Markdown \u7684\u63D2\u4EF6\uFF09",
|
|
418
|
-
aiAsk: "\u63A8\u8350",
|
|
419
|
-
aiAsking: "AI \u601D\u8003\u4E2D\u2026",
|
|
420
|
-
aiFail: "AI \u63A8\u8350\u5931\u8D25\uFF1A{e}",
|
|
421
416
|
scoreLabel: "\u8BC4\u5206",
|
|
422
417
|
screenshot: "\u622A\u56FE",
|
|
423
418
|
noScreenshot: "\u65E0\u622A\u56FE",
|
|
@@ -492,11 +487,6 @@ var STRINGS = {
|
|
|
492
487
|
diagInstalled: "Installed plugins ({n})",
|
|
493
488
|
diagDisabled: "Disabled state",
|
|
494
489
|
diagPnpmLog: "pnpm log (tail)",
|
|
495
|
-
aiTitle: "AI recommendation",
|
|
496
|
-
aiPlaceholder: "Describe what you need \u2014 e.g. a Markdown preview plugin\u2026",
|
|
497
|
-
aiAsk: "Recommend",
|
|
498
|
-
aiAsking: "AI is thinking\u2026",
|
|
499
|
-
aiFail: "AI recommendation failed: {e}",
|
|
500
490
|
scoreLabel: "Score",
|
|
501
491
|
screenshot: "Screenshot",
|
|
502
492
|
noScreenshot: "No screenshot",
|
|
@@ -667,6 +657,7 @@ function MarketView({ category, single, source, search, onCount }) {
|
|
|
667
657
|
void rpc("install", { spec: m.spec }).then(
|
|
668
658
|
(v) => {
|
|
669
659
|
const durationMs = typeof v === "object" && v !== null ? v.durationMs : void 0;
|
|
660
|
+
const detail = typeof v === "object" && v !== null ? v.detail : void 0;
|
|
670
661
|
if (v !== true && durationMs === void 0) throw new Error(t("installNotApplied"));
|
|
671
662
|
setBusy(null);
|
|
672
663
|
pendingInstall.add(m.spec);
|
|
@@ -675,7 +666,7 @@ function MarketView({ category, single, source, search, onCount }) {
|
|
|
675
666
|
if (c !== void 0) c.plugins = c.plugins.map((p) => p.name === m.name ? { ...p, installed: true } : p);
|
|
676
667
|
}
|
|
677
668
|
setItems((prev) => prev.map((p) => p.name === m.name ? { ...p, installed: true } : p));
|
|
678
|
-
showToast(t("installQueued", { n: m.name }) + (durationMs !== void 0 ? `\uFF08${(durationMs / 1e3).toFixed(1)}s\uFF09` : ""), "ok",
|
|
669
|
+
showToast(detail ?? t("installQueued", { n: m.name }) + (durationMs !== void 0 ? `\uFF08${(durationMs / 1e3).toFixed(1)}s\uFF09` : ""), "ok", 7e3);
|
|
679
670
|
},
|
|
680
671
|
(e) => {
|
|
681
672
|
setBusy(null);
|
|
@@ -728,7 +719,11 @@ function MarketView({ category, single, source, search, onCount }) {
|
|
|
728
719
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
|
|
729
720
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "pc-iconbtn", title: t("screenshot"), "aria-label": t("screenshot"), disabled: shotBusy !== null, onClick: () => {
|
|
730
721
|
showScreenshot(m);
|
|
731
|
-
}, children: shotBusy === m.name ? "\u2026" : "
|
|
722
|
+
}, children: shotBusy === m.name ? "\u2026" : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: [
|
|
723
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "3.5", width: "12", height: "9", rx: "1.5" }),
|
|
724
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8", cy: "8", r: "2.6" }),
|
|
725
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5.5 3.5 6.4 1.8h3.2l.9 1.7" })
|
|
726
|
+
] }) }),
|
|
732
727
|
m.installed || pendingInstall.has(m.spec) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: pendingInstall.has(m.spec) ? t("pendingRestart") : t("installedTag") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: busy !== null, onClick: () => {
|
|
733
728
|
install(m);
|
|
734
729
|
}, children: busy === m.name ? t("installing") : t("install") })
|
|
@@ -892,10 +887,6 @@ function CenterPanel({ variant = "section" }) {
|
|
|
892
887
|
const [busyUpdate, setBusyUpdate] = (0, import_react.useState)(null);
|
|
893
888
|
const [checking, setChecking] = (0, import_react.useState)(false);
|
|
894
889
|
const [togglingId, setTogglingId] = (0, import_react.useState)(null);
|
|
895
|
-
const [aiQuery, setAiQuery] = (0, import_react.useState)("");
|
|
896
|
-
const [aiBusy, setAiBusy] = (0, import_react.useState)(false);
|
|
897
|
-
const [aiResult, setAiResult] = (0, import_react.useState)(null);
|
|
898
|
-
const [aiError, setAiError] = (0, import_react.useState)(null);
|
|
899
890
|
const handleToggle = (p) => {
|
|
900
891
|
if (togglingId !== null) return;
|
|
901
892
|
const id = p.entryId;
|
|
@@ -929,23 +920,6 @@ function CenterPanel({ variant = "section" }) {
|
|
|
929
920
|
}
|
|
930
921
|
);
|
|
931
922
|
};
|
|
932
|
-
const askAi = () => {
|
|
933
|
-
const q = aiQuery.trim();
|
|
934
|
-
if (q === "" || aiBusy) return;
|
|
935
|
-
setAiBusy(true);
|
|
936
|
-
setAiResult(null);
|
|
937
|
-
setAiError(null);
|
|
938
|
-
void rpc("suggest", { query: q }).then(
|
|
939
|
-
(v) => {
|
|
940
|
-
setAiBusy(false);
|
|
941
|
-
setAiResult(v);
|
|
942
|
-
},
|
|
943
|
-
(e) => {
|
|
944
|
-
setAiBusy(false);
|
|
945
|
-
setAiError(e instanceof Error ? e.message : String(e));
|
|
946
|
-
}
|
|
947
|
-
);
|
|
948
|
-
};
|
|
949
923
|
const refreshUpdates = (0, import_react.useCallback)((silent = false) => {
|
|
950
924
|
setChecking(true);
|
|
951
925
|
void rpc("checkUpdates", { since: new Date(Date.now() - 30 * 864e5).toISOString() }).then(
|
|
@@ -1104,63 +1078,7 @@ function CenterPanel({ variant = "section" }) {
|
|
|
1104
1078
|
}, children: c }, c))
|
|
1105
1079
|
] })
|
|
1106
1080
|
] }) : null;
|
|
1107
|
-
const aiBar = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6, flex: "none" }, children: [
|
|
1108
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-ai-row", children: [
|
|
1109
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1110
|
-
"input",
|
|
1111
|
-
{
|
|
1112
|
-
className: "pc-search",
|
|
1113
|
-
style: { flex: 1 },
|
|
1114
|
-
value: aiQuery,
|
|
1115
|
-
onChange: (e) => {
|
|
1116
|
-
setAiQuery(e.target.value);
|
|
1117
|
-
},
|
|
1118
|
-
onKeyDown: (e) => {
|
|
1119
|
-
if (e.key === "Enter") askAi();
|
|
1120
|
-
},
|
|
1121
|
-
placeholder: t("aiPlaceholder")
|
|
1122
|
-
}
|
|
1123
|
-
),
|
|
1124
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: aiBusy || aiQuery.trim() === "", onClick: askAi, children: aiBusy ? t("aiAsking") : t("aiAsk") })
|
|
1125
|
-
] }),
|
|
1126
|
-
aiError !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("aiFail", { e: aiError }) }),
|
|
1127
|
-
aiResult !== null && aiResult.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-grid single", children: aiResult.map((item) => {
|
|
1128
|
-
const installed = (installedCache ?? []).some((p) => p.name === item.name || p.spec === item.spec || p.name === item.spec);
|
|
1129
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-card", children: [
|
|
1130
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-row", children: [
|
|
1131
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-name", children: item.name }),
|
|
1132
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
|
|
1133
|
-
item.stars != null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "pc-ver", children: [
|
|
1134
|
-
"\u2605 ",
|
|
1135
|
-
item.stars
|
|
1136
|
-
] }),
|
|
1137
|
-
installed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: t("installedTag") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1138
|
-
"button",
|
|
1139
|
-
{
|
|
1140
|
-
className: "pc-btn primary",
|
|
1141
|
-
onClick: () => {
|
|
1142
|
-
if (item.spec === void 0 || item.spec === null || item.spec === "") {
|
|
1143
|
-
showToast(t("installFailed", { e: "\u672A\u627E\u5230\u8BE5\u63D2\u4EF6\u7684\u5B89\u88C5 spec" }), "error", 8e3);
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
void rpc("install", { spec: item.spec }).then(
|
|
1147
|
-
() => {
|
|
1148
|
-
pendingInstall.add(item.spec);
|
|
1149
|
-
showToast(t("installQueued", { n: item.name }), "ok", 5e3);
|
|
1150
|
-
},
|
|
1151
|
-
(e) => showToast(t("installFailed", { e: e instanceof Error ? e.message : String(e) }), "error", 15e3)
|
|
1152
|
-
);
|
|
1153
|
-
},
|
|
1154
|
-
children: t("install")
|
|
1155
|
-
}
|
|
1156
|
-
)
|
|
1157
|
-
] }),
|
|
1158
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-desc", children: item.reason })
|
|
1159
|
-
] }, item.name);
|
|
1160
|
-
}) })
|
|
1161
|
-
] });
|
|
1162
1081
|
const marketToolbar = view === "market" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-filter", children: [
|
|
1163
|
-
aiBar,
|
|
1164
1082
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-toolbar pc-toolbar-main", children: [
|
|
1165
1083
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "pc-search", style: { flex: 1, minWidth: 120 }, value: marketSearch, onChange: (e) => {
|
|
1166
1084
|
setMarketSearch(e.target.value);
|
|
@@ -1171,11 +1089,7 @@ function CenterPanel({ variant = "section" }) {
|
|
|
1171
1089
|
}, children: [
|
|
1172
1090
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "awesome", children: "awesome-dsh-plugin" }),
|
|
1173
1091
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "oh-my-dsh", children: "Oh-My-DSH" }),
|
|
1174
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
1175
|
-
"dsh-market\uFF08",
|
|
1176
|
-
counts.dshMarket,
|
|
1177
|
-
"\uFF09"
|
|
1178
|
-
] }),
|
|
1092
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "dsh-market", children: "dsh-market" }),
|
|
1179
1093
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: t("allMarkets") })
|
|
1180
1094
|
] }),
|
|
1181
1095
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
|
package/dist/engine.d.ts
CHANGED
|
@@ -45,12 +45,17 @@ export declare class PluginCenterEngine extends Service {
|
|
|
45
45
|
private awesomeCache;
|
|
46
46
|
private awesomeDone;
|
|
47
47
|
private awesomeFetching;
|
|
48
|
+
/** 上次 fetch 失败时刻(0=未失败过):失败后冷却 60s 再重试,
|
|
49
|
+
* 防止瞬时网络故障让源永久失效(2026-08-22 dsh-market(0)实测)。 */
|
|
50
|
+
private awesomeFailedAt;
|
|
48
51
|
private ohMyDshCache;
|
|
49
52
|
private ohMyDshDone;
|
|
50
53
|
private ohMyDshFetching;
|
|
54
|
+
private ohMyDshFailedAt;
|
|
51
55
|
private dshMarketCache;
|
|
52
56
|
private dshMarketDone;
|
|
53
57
|
private dshMarketFetching;
|
|
58
|
+
private dshMarketFailedAt;
|
|
54
59
|
/** README-extracted screenshot URL per plugin name (lazy, P2). */
|
|
55
60
|
private readonly screenshotCache;
|
|
56
61
|
private installedNamesCache;
|
|
@@ -74,15 +79,15 @@ export declare class PluginCenterEngine extends Service {
|
|
|
74
79
|
dshVersion(): Promise<string>;
|
|
75
80
|
/** Non-group Loader entries, cross-matched with market categories. */
|
|
76
81
|
listInstalled(): Promise<InstalledPlugin[]>;
|
|
77
|
-
/** Start the awesome catalog fetch
|
|
82
|
+
/** Start the awesome catalog fetch (with failed-retry cooldown). */
|
|
78
83
|
private prefetchAwesome;
|
|
79
84
|
/** Backfill npm latest versions into the awesome cache (best-effort, concurrent). */
|
|
80
85
|
private fillNpmVersions;
|
|
81
86
|
/** Single-registry latest version for the market bulk backfill (npmmirror is fast). */
|
|
82
87
|
private fastNpmVersion;
|
|
83
|
-
/** Start the Oh-My-DSH fetch
|
|
88
|
+
/** Start the Oh-My-DSH fetch (single PLUGINS.md parse, with failed-retry cooldown). */
|
|
84
89
|
private prefetchOhMyDsh;
|
|
85
|
-
/** Start the dsh-market fetch
|
|
90
|
+
/** Start the dsh-market fetch (2BingLing/dsh-market, ~3900 plugins, trimmed, with failed-retry cooldown). */
|
|
86
91
|
private prefetchDshMarket;
|
|
87
92
|
/** Installed plugin names (no file IO) — cached so market polling stays cheap. */
|
|
88
93
|
private installedNames;
|
|
@@ -116,6 +121,8 @@ export declare class PluginCenterEngine extends Service {
|
|
|
116
121
|
screenshot(name: string): Promise<string | null>;
|
|
117
122
|
/** AI recommendation: keyword-filtered candidates ranked by the model. */
|
|
118
123
|
suggest(query: string): Promise<Suggestion[]>;
|
|
119
|
-
/** Wait for
|
|
120
|
-
private
|
|
124
|
+
/** Wait for all three market sources (fetch or failure), with a hard deadline. */
|
|
125
|
+
private waitAllSources;
|
|
126
|
+
/** 三源市场合并(按 name 去重;spec 优先 npm 名——比 github 回退更可靠)。 */
|
|
127
|
+
private combinedMarketCache;
|
|
121
128
|
}
|
package/dist/engine.js
CHANGED
|
@@ -13,6 +13,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
13
13
|
import { buildInstalledPlugin, clearPackageCache, resolvePackage } from "./meta.js";
|
|
14
14
|
import { fetchAwesomePluginsJson, fetchDshMarketPlugins, fetchOhMyDshOverrides, fetchOhMyDshPlugins, mapConcurrent, mergePlugins, } from "./market.js";
|
|
15
15
|
import { detectUpdate, installPlugin, updatePlugin } from "./update.js";
|
|
16
|
+
import { reconcileInstalled, readDependencyKeys } from "./reconcile.js";
|
|
16
17
|
import { readDisabledState, setDisabled } from "./toggle.js";
|
|
17
18
|
/** Runtime mirror of cordis FiberState (a cross-package const enum). */
|
|
18
19
|
const FIBER_PHASE = {
|
|
@@ -62,12 +63,17 @@ export class PluginCenterEngine extends Service {
|
|
|
62
63
|
awesomeCache = [];
|
|
63
64
|
awesomeDone = false;
|
|
64
65
|
awesomeFetching = false;
|
|
66
|
+
/** 上次 fetch 失败时刻(0=未失败过):失败后冷却 60s 再重试,
|
|
67
|
+
* 防止瞬时网络故障让源永久失效(2026-08-22 dsh-market(0)实测)。 */
|
|
68
|
+
awesomeFailedAt = 0;
|
|
65
69
|
ohMyDshCache = [];
|
|
66
70
|
ohMyDshDone = false;
|
|
67
71
|
ohMyDshFetching = false;
|
|
72
|
+
ohMyDshFailedAt = 0;
|
|
68
73
|
dshMarketCache = [];
|
|
69
74
|
dshMarketDone = false;
|
|
70
75
|
dshMarketFetching = false;
|
|
76
|
+
dshMarketFailedAt = 0;
|
|
71
77
|
/** README-extracted screenshot URL per plugin name (lazy, P2). */
|
|
72
78
|
screenshotCache = new Map();
|
|
73
79
|
installedNamesCache = null;
|
|
@@ -155,11 +161,16 @@ export class PluginCenterEngine extends Service {
|
|
|
155
161
|
.map(p => ({ ...p, categories: categoryByName.get(p.name) ?? [] }))
|
|
156
162
|
.sort((a, b) => SOURCE_ORDER[a.source] - SOURCE_ORDER[b.source]);
|
|
157
163
|
}
|
|
158
|
-
/** Start the awesome catalog fetch
|
|
164
|
+
/** Start the awesome catalog fetch (with failed-retry cooldown). */
|
|
159
165
|
prefetchAwesome() {
|
|
160
|
-
if (this.awesomeFetching
|
|
166
|
+
if (this.awesomeFetching)
|
|
167
|
+
return;
|
|
168
|
+
if (this.awesomeDone && this.awesomeCache.length > 0)
|
|
169
|
+
return;
|
|
170
|
+
if (this.awesomeDone && Date.now() - this.awesomeFailedAt < 60_000)
|
|
161
171
|
return;
|
|
162
172
|
this.awesomeFetching = true;
|
|
173
|
+
this.awesomeDone = false;
|
|
163
174
|
void (async () => {
|
|
164
175
|
try {
|
|
165
176
|
const [plugins, overrides] = await Promise.all([fetchAwesomePluginsJson(), fetchOhMyDshOverrides()]);
|
|
@@ -172,7 +183,9 @@ export class PluginCenterEngine extends Service {
|
|
|
172
183
|
this.awesomeCache = merged;
|
|
173
184
|
await this.fillNpmVersions();
|
|
174
185
|
}
|
|
175
|
-
catch {
|
|
186
|
+
catch {
|
|
187
|
+
this.awesomeFailedAt = Date.now();
|
|
188
|
+
}
|
|
176
189
|
this.awesomeDone = true;
|
|
177
190
|
this.awesomeFetching = false;
|
|
178
191
|
})();
|
|
@@ -205,30 +218,44 @@ export class PluginCenterEngine extends Service {
|
|
|
205
218
|
return null;
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
|
-
/** Start the Oh-My-DSH fetch
|
|
221
|
+
/** Start the Oh-My-DSH fetch (single PLUGINS.md parse, with failed-retry cooldown). */
|
|
209
222
|
prefetchOhMyDsh() {
|
|
210
|
-
if (this.ohMyDshFetching
|
|
223
|
+
if (this.ohMyDshFetching)
|
|
224
|
+
return;
|
|
225
|
+
if (this.ohMyDshDone && this.ohMyDshCache.length > 0)
|
|
226
|
+
return;
|
|
227
|
+
if (this.ohMyDshDone && Date.now() - this.ohMyDshFailedAt < 60_000)
|
|
211
228
|
return;
|
|
212
229
|
this.ohMyDshFetching = true;
|
|
230
|
+
this.ohMyDshDone = false;
|
|
213
231
|
void (async () => {
|
|
214
232
|
try {
|
|
215
233
|
this.ohMyDshCache = mergePlugins([await fetchOhMyDshPlugins()]);
|
|
216
234
|
}
|
|
217
|
-
catch {
|
|
235
|
+
catch {
|
|
236
|
+
this.ohMyDshFailedAt = Date.now();
|
|
237
|
+
}
|
|
218
238
|
this.ohMyDshDone = true;
|
|
219
239
|
this.ohMyDshFetching = false;
|
|
220
240
|
})();
|
|
221
241
|
}
|
|
222
|
-
/** Start the dsh-market fetch
|
|
242
|
+
/** Start the dsh-market fetch (2BingLing/dsh-market, ~3900 plugins, trimmed, with failed-retry cooldown). */
|
|
223
243
|
prefetchDshMarket() {
|
|
224
|
-
if (this.dshMarketFetching
|
|
244
|
+
if (this.dshMarketFetching)
|
|
245
|
+
return;
|
|
246
|
+
if (this.dshMarketDone && this.dshMarketCache.length > 0)
|
|
247
|
+
return;
|
|
248
|
+
if (this.dshMarketDone && Date.now() - this.dshMarketFailedAt < 60_000)
|
|
225
249
|
return;
|
|
226
250
|
this.dshMarketFetching = true;
|
|
251
|
+
this.dshMarketDone = false;
|
|
227
252
|
void (async () => {
|
|
228
253
|
try {
|
|
229
254
|
this.dshMarketCache = mergePlugins([await fetchDshMarketPlugins()]);
|
|
230
255
|
}
|
|
231
|
-
catch {
|
|
256
|
+
catch {
|
|
257
|
+
this.dshMarketFailedAt = Date.now();
|
|
258
|
+
}
|
|
232
259
|
this.dshMarketDone = true;
|
|
233
260
|
this.dshMarketFetching = false;
|
|
234
261
|
})();
|
|
@@ -251,18 +278,18 @@ export class PluginCenterEngine extends Service {
|
|
|
251
278
|
const decorate = (plugins) => plugins.map(p => ({ ...p, installed: installedNames.has(p.name) || installedNames.has(p.spec) }));
|
|
252
279
|
if (source === 'awesome') {
|
|
253
280
|
this.prefetchAwesome();
|
|
254
|
-
return { plugins: decorate(this.awesomeCache), done: this.awesomeDone };
|
|
281
|
+
return { plugins: decorate(this.awesomeCache), done: this.awesomeDone && this.awesomeCache.length > 0 };
|
|
255
282
|
}
|
|
256
283
|
if (source === 'oh-my-dsh') {
|
|
257
284
|
this.prefetchOhMyDsh();
|
|
258
|
-
return { plugins: decorate(this.ohMyDshCache), done: this.ohMyDshDone };
|
|
285
|
+
return { plugins: decorate(this.ohMyDshCache), done: this.ohMyDshDone && this.ohMyDshCache.length > 0 };
|
|
259
286
|
}
|
|
260
287
|
if (source === 'dsh-market') {
|
|
261
288
|
this.prefetchDshMarket();
|
|
262
289
|
// Score-first order (best first) so the big catalog reads usefully.
|
|
263
290
|
return {
|
|
264
291
|
plugins: decorate(this.dshMarketCache).sort((a, b) => (b.score?.total ?? 0) - (a.score?.total ?? 0)),
|
|
265
|
-
done: this.dshMarketDone,
|
|
292
|
+
done: this.dshMarketDone && this.dshMarketCache.length > 0,
|
|
266
293
|
};
|
|
267
294
|
}
|
|
268
295
|
this.prefetchAwesome();
|
|
@@ -270,7 +297,9 @@ export class PluginCenterEngine extends Service {
|
|
|
270
297
|
this.prefetchDshMarket();
|
|
271
298
|
return {
|
|
272
299
|
plugins: decorate(mergePlugins([this.awesomeCache, this.ohMyDshCache, this.dshMarketCache])),
|
|
273
|
-
done: this.awesomeDone && this.
|
|
300
|
+
done: (this.awesomeDone && this.awesomeCache.length > 0)
|
|
301
|
+
&& (this.ohMyDshDone && this.ohMyDshCache.length > 0)
|
|
302
|
+
&& (this.dshMarketDone && this.dshMarketCache.length > 0),
|
|
274
303
|
};
|
|
275
304
|
}
|
|
276
305
|
/** Detect updates for every installed third-party/local plugin, TTL-cached. */
|
|
@@ -286,7 +315,25 @@ export class PluginCenterEngine extends Service {
|
|
|
286
315
|
return this.updatesCache.digests;
|
|
287
316
|
}
|
|
288
317
|
async install(spec) {
|
|
289
|
-
return this.enqueuePnpm(() =>
|
|
318
|
+
return this.enqueuePnpm(async () => {
|
|
319
|
+
// Snapshot inside the serialized chain: concurrent installs would
|
|
320
|
+
// otherwise diff each other's additions.
|
|
321
|
+
const before = readDependencyKeys(this.baseUrl);
|
|
322
|
+
const result = await installPlugin(spec, this.baseUrl);
|
|
323
|
+
if (result.ok) {
|
|
324
|
+
// `dsh plugin add` reconciles bundles into dsh.profile.bundles and
|
|
325
|
+
// reports non-plugin repos; mirror that so "restart to take effect"
|
|
326
|
+
// is true for real bundles and honest for repo-only packages
|
|
327
|
+
// (2026-08-22: EAC 装完不生效——无插件入口,提示误导).
|
|
328
|
+
try {
|
|
329
|
+
const { note } = reconcileInstalled(this.baseUrl, before);
|
|
330
|
+
if (note !== '')
|
|
331
|
+
result.detail = note;
|
|
332
|
+
}
|
|
333
|
+
catch { /* best-effort */ }
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
});
|
|
290
337
|
}
|
|
291
338
|
/** Update one installed plugin to the detected target version (exact — see update.ts). */
|
|
292
339
|
async update(name, version) {
|
|
@@ -356,13 +403,15 @@ export class PluginCenterEngine extends Service {
|
|
|
356
403
|
const q = query.trim();
|
|
357
404
|
if (q === '')
|
|
358
405
|
return [];
|
|
359
|
-
await this.
|
|
406
|
+
await this.waitAllSources();
|
|
360
407
|
const llm = this.ctx.get('llm');
|
|
361
408
|
if (llm?.stream === undefined) {
|
|
362
409
|
throw new Error('llm 服务不可用(当前 profile 未提供 dsh-llm)');
|
|
363
410
|
}
|
|
364
411
|
const tokens = q.toLowerCase().split(/[\s,,、;;]+/u).filter(Boolean);
|
|
365
|
-
|
|
412
|
+
// 候选 = 三源合并(2026-08-22:此前只用 dsh-market,awesome/Oh-My-DSH
|
|
413
|
+
// 的精选插件进不了推荐;合并后按 name 去重、spec 优先 npm 名)。
|
|
414
|
+
const scored = this.combinedMarketCache()
|
|
366
415
|
.map(p => ({
|
|
367
416
|
p,
|
|
368
417
|
hits: tokens.reduce((n, tok) => n + (p.name.toLowerCase().includes(tok)
|
|
@@ -375,7 +424,7 @@ export class PluginCenterEngine extends Service {
|
|
|
375
424
|
if (scored.length === 0)
|
|
376
425
|
return [];
|
|
377
426
|
const list = scored.map(({ p }) => `- ${p.name} | ${p.stars ?? 0}★ | ${p.description.zh.slice(0, 60)}`).join('\n');
|
|
378
|
-
const system = '你是 DeepSeek Harness 插件市场的推荐助手。根据用户需求从候选插件中选择 3-5 个最合适的,只输出一个 JSON 数组(不要 markdown 代码块、不要任何多余文字):[{"name":"插件名","reason":"一句话中文推荐理由(20 字以内)"}]';
|
|
427
|
+
const system = '你是 DeepSeek Harness 插件市场的推荐助手。根据用户需求从候选插件中选择 3-5 个最合适的,只输出一个 JSON 数组(不要 markdown 代码块、不要任何多余文字):[{"name":"插件名","reason":"一句话中文推荐理由(20 字以内)"}]。名称必须从候选列表原样复制,禁止改写、拼接或编造';
|
|
379
428
|
// Message 契约:content 是 ContentBlock 数组(非字符串),且需要 id/source。
|
|
380
429
|
const chunks = llm.stream({
|
|
381
430
|
provider: 'deepseek-official',
|
|
@@ -428,7 +477,14 @@ export class PluginCenterEngine extends Service {
|
|
|
428
477
|
&& typeof item.reason === 'string')
|
|
429
478
|
.slice(0, 5)
|
|
430
479
|
.map(item => {
|
|
431
|
-
|
|
480
|
+
// 模型可能改写 owner/路径(幻觉,如 zhu1090093659 非真实 owner)
|
|
481
|
+
// 或输出 monorepo 子路径——先精确匹配(name/spec/npm),再 # 截断,
|
|
482
|
+
// 最后回退 repo 名匹配(2026-08-22:DamonKoy/dsh-web-ui#dsh-web-ui-all
|
|
483
|
+
// 被输出成 zhu1090093659/dsh-web-ui#packages/...,精确匹配失败无星)。
|
|
484
|
+
const base = item.name.split('#')[0].trim();
|
|
485
|
+
const repo = base.split('/').pop() ?? '';
|
|
486
|
+
const hit = this.combinedMarketCache().find(p => p.name === base || p.spec === base || p.npm === base
|
|
487
|
+
|| (p.name.split('/').pop()?.split('#')[0] ?? '') === repo);
|
|
432
488
|
return {
|
|
433
489
|
name: item.name,
|
|
434
490
|
reason: item.reason,
|
|
@@ -442,12 +498,38 @@ export class PluginCenterEngine extends Service {
|
|
|
442
498
|
throw new Error(`模型输出无法解析:${text.slice(0, 400)}`);
|
|
443
499
|
}
|
|
444
500
|
}
|
|
445
|
-
/** Wait for
|
|
446
|
-
async
|
|
501
|
+
/** Wait for all three market sources (fetch or failure), with a hard deadline. */
|
|
502
|
+
async waitAllSources() {
|
|
503
|
+
this.prefetchAwesome();
|
|
504
|
+
this.prefetchOhMyDsh();
|
|
447
505
|
this.prefetchDshMarket();
|
|
448
506
|
const deadline = Date.now() + 65_000;
|
|
449
|
-
while (!this.dshMarketDone && Date.now() < deadline) {
|
|
507
|
+
while (!(this.awesomeDone && this.ohMyDshDone && this.dshMarketDone) && Date.now() < deadline) {
|
|
450
508
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
451
509
|
}
|
|
452
510
|
}
|
|
511
|
+
/** 三源市场合并(按 name 去重;spec 优先 npm 名——比 github 回退更可靠)。 */
|
|
512
|
+
combinedMarketCache() {
|
|
513
|
+
const map = new Map();
|
|
514
|
+
for (const list of [this.awesomeCache, this.ohMyDshCache, this.dshMarketCache]) {
|
|
515
|
+
for (const p of list) {
|
|
516
|
+
const cur = map.get(p.name);
|
|
517
|
+
if (cur === undefined) {
|
|
518
|
+
map.set(p.name, p);
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
map.set(p.name, {
|
|
522
|
+
...cur,
|
|
523
|
+
spec: p.npm ?? cur.npm ?? p.spec ?? cur.spec,
|
|
524
|
+
npm: cur.npm ?? p.npm,
|
|
525
|
+
// 同一条目多源星数不同:取最大值(2026-08-22:awesome ★4 曾
|
|
526
|
+
// 覆盖 dsh-market 高星——合并顺序导致低星胜出)。
|
|
527
|
+
stars: Math.max(cur.stars ?? 0, p.stars ?? 0) || null,
|
|
528
|
+
score: cur.score ?? p.score,
|
|
529
|
+
description: cur.description.zh !== '' && cur.description.en !== '' ? cur.description : p.description,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return [...map.values()];
|
|
534
|
+
}
|
|
453
535
|
}
|
package/dist/market.js
CHANGED
|
@@ -108,9 +108,12 @@ function categorizeTags(tags) {
|
|
|
108
108
|
* file is ~10 MB, so the trimmed result (~1.5 MB) is what the engine caches.
|
|
109
109
|
*/
|
|
110
110
|
export async function fetchDshMarketPlugins() {
|
|
111
|
+
// 10MB 聚合文件 + raw.githubusercontent 不稳定:60s 曾实测超时失败
|
|
112
|
+
// (2026-08-22 用户看到 dsh-market(0)——失败后无重试,永远空),
|
|
113
|
+
// 超时放宽到 120s。
|
|
111
114
|
const res = await fetch('https://raw.githubusercontent.com/2BingLing/dsh-market/master/data/plugins.json', {
|
|
112
115
|
headers: UA,
|
|
113
|
-
signal: AbortSignal.timeout(
|
|
116
|
+
signal: AbortSignal.timeout(120000),
|
|
114
117
|
});
|
|
115
118
|
if (!res.ok)
|
|
116
119
|
throw new Error(`dsh-market plugins.json: HTTP ${res.status}`);
|
|
@@ -122,6 +125,12 @@ export async function fetchDshMarketPlugins() {
|
|
|
122
125
|
// are excluded from the catalog rather than offered with a broken spec.
|
|
123
126
|
if (p.type === 'skill' || p.install?.method === 'skills-add')
|
|
124
127
|
continue;
|
|
128
|
+
// Entries without README install commands are usually desktop apps or
|
|
129
|
+
// config repos, not installable plugins (2026-08-22: 4299 条里 943 条
|
|
130
|
+
// 无 commands——如 Deepseek-Harness-EAC「一键启动桌面端」被当插件
|
|
131
|
+
// 推荐,装完不可用;installSpecOf 只能 fallback 一个假 spec)。
|
|
132
|
+
if (!Array.isArray(p.install?.commands) || p.install.commands.length === 0)
|
|
133
|
+
continue;
|
|
125
134
|
const name = typeof p.name === 'string' && p.name !== '' ? p.name : '';
|
|
126
135
|
const full = typeof p.fullName === 'string' && p.fullName !== '' ? p.fullName : name;
|
|
127
136
|
if (full === '')
|
|
@@ -167,10 +176,15 @@ export async function fetchOhMyDshPlugins() {
|
|
|
167
176
|
if (link === null)
|
|
168
177
|
continue;
|
|
169
178
|
const stars = Number(cells[5]);
|
|
179
|
+
// spec 必须用 URL 的 owner/repo——链接文本常只是包名(如
|
|
180
|
+
// [open-design](https://github.com/nexu-io/open-design)),
|
|
181
|
+
// github:open-design 是无效 spec(2026-08-22 实测:Oh-My-DSH
|
|
182
|
+
// 全部条目 spec 错,open-design 装不上)。
|
|
183
|
+
const repo = /github\.com\/([^/]+\/[^/]+)/u.exec(link[2]);
|
|
170
184
|
plugins.push({
|
|
171
185
|
name: link[1],
|
|
172
186
|
url: link[2],
|
|
173
|
-
spec: `github:${link[1]}`,
|
|
187
|
+
spec: `github:${repo?.[1] ?? link[1]}`,
|
|
174
188
|
categories: category !== '' ? [category] : [],
|
|
175
189
|
description: { en: '', zh: cells[6] },
|
|
176
190
|
stars: Number.isFinite(stars) ? stars : null,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Snapshot of the profile's declared dependencies (keys only). */
|
|
2
|
+
export declare function readDependencyKeys(profileDir: string): ReadonlySet<string>;
|
|
3
|
+
/** Human feedback for the install toast, joined by `;`. */
|
|
4
|
+
export interface ReconcileOutcome {
|
|
5
|
+
note: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Compare the dependency set against `before`; for each new entry, read the
|
|
9
|
+
* installed manifest and either register the bundle or report the truth.
|
|
10
|
+
* Best-effort: never throws (the install itself already succeeded).
|
|
11
|
+
*/
|
|
12
|
+
export declare function reconcileInstalled(profileDir: string, before: ReadonlySet<string>): ReconcileOutcome;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-install reconciliation: after a successful `pnpm add`, figure out
|
|
3
|
+
* which dependency actually appeared, register it as a bundle when the
|
|
4
|
+
* package ships `dsh.bundle.patch` (mirroring `dsh plugin add`'s
|
|
5
|
+
* reconcilePlugins — without this the loader never mounts the plugin and
|
|
6
|
+
* "restart to take effect" is a lie), and honestly report repos that are
|
|
7
|
+
* NOT plugin packages at all (no manifest, e.g. pnpm's placeholder for a
|
|
8
|
+
* repo without package.json like Deepseek-Harness-EAC, 2026-08-22).
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
/** Snapshot of the profile's declared dependencies (keys only). */
|
|
13
|
+
export function readDependencyKeys(profileDir) {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'));
|
|
16
|
+
return new Set(Object.keys(pkg.dependencies ?? {}));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return new Set();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Compare the dependency set against `before`; for each new entry, read the
|
|
24
|
+
* installed manifest and either register the bundle or report the truth.
|
|
25
|
+
* Best-effort: never throws (the install itself already succeeded).
|
|
26
|
+
*/
|
|
27
|
+
export function reconcileInstalled(profileDir, before) {
|
|
28
|
+
const notes = [];
|
|
29
|
+
const pkgPath = join(profileDir, 'package.json');
|
|
30
|
+
let pkg;
|
|
31
|
+
try {
|
|
32
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { note: '' };
|
|
36
|
+
}
|
|
37
|
+
const added = Object.keys(pkg.dependencies ?? {}).filter(name => !before.has(name));
|
|
38
|
+
for (const name of added) {
|
|
39
|
+
const dir = join(profileDir, 'node_modules', name);
|
|
40
|
+
if (!existsSync(dir))
|
|
41
|
+
continue;
|
|
42
|
+
let manifest = null;
|
|
43
|
+
try {
|
|
44
|
+
manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
manifest = null;
|
|
48
|
+
}
|
|
49
|
+
if (manifest === null || typeof manifest.name !== 'string' || manifest._pnpmPlaceholder !== undefined) {
|
|
50
|
+
notes.push(`「${name}」已下载,但该仓库不是插件包(无插件入口),不会被 DSH 加载`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const isBundle = manifest.dsh?.bundle?.patch !== undefined;
|
|
54
|
+
if (isBundle) {
|
|
55
|
+
const bundles = pkg.dsh?.profile?.bundles;
|
|
56
|
+
if (!Array.isArray(bundles) || !bundles.includes(name)) {
|
|
57
|
+
if (!pkg.dsh)
|
|
58
|
+
pkg.dsh = {};
|
|
59
|
+
if (!pkg.dsh.profile)
|
|
60
|
+
pkg.dsh.profile = {};
|
|
61
|
+
const list = Array.isArray(bundles) ? bundles : [];
|
|
62
|
+
list.push(name);
|
|
63
|
+
pkg.dsh.profile.bundles = list;
|
|
64
|
+
try {
|
|
65
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
66
|
+
notes.push(`「${name}」已安装并注册,重启后生效`);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
notes.push(`「${name}」已安装(注册失败,需手动加入 dsh.profile.bundles)`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
notes.push(`「${name}」已安装`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
notes.push(`「${name}」已安装为依赖(非 bundle 插件,未注册加载)`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { note: notes.join(';') };
|
|
81
|
+
}
|
package/dist/rpc.js
CHANGED
|
@@ -31,7 +31,7 @@ export class PluginCenterRpc extends Service {
|
|
|
31
31
|
const result = await ctx.pluginCenter.install(spec);
|
|
32
32
|
if (!result.ok)
|
|
33
33
|
return internal(`install ${spec} 失败:${result.detail}`);
|
|
34
|
-
return { ok: true, value: { durationMs: result.durationMs } };
|
|
34
|
+
return { ok: true, value: { durationMs: result.durationMs, detail: result.detail } };
|
|
35
35
|
}
|
|
36
36
|
case 'update': {
|
|
37
37
|
const name = payload?.name;
|
package/dist/toggle.d.ts
CHANGED
|
@@ -28,4 +28,4 @@ export declare function readDisabledState(patchPath: string): Map<string, boolea
|
|
|
28
28
|
* @param disabled - the target stance.
|
|
29
29
|
* @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
|
|
30
30
|
*/
|
|
31
|
-
export declare function setDisabled(profileDir: string,
|
|
31
|
+
export declare function setDisabled(profileDir: string, entryId: string, disabled: boolean): Promise<ToggleResult>;
|
package/dist/toggle.js
CHANGED
|
@@ -33,6 +33,17 @@ const PROTECTED_PATTERNS = [
|
|
|
33
33
|
function isProtected(id) {
|
|
34
34
|
return PROTECTED_PATTERNS.some(pattern => pattern.test(id));
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Patch rows address a loader row by its ORIGINAL patch id, but the Loader
|
|
38
|
+
* expands `insert` children into runtime ids like `include:<id>`
|
|
39
|
+
* (2026-08-22 chinese-thinking 实证:写入 `include:chinese-thinking` 行
|
|
40
|
+
* 组合后不匹配任何行 → 禁用不生效;写入 `chinese-thinking` 即生效)。
|
|
41
|
+
* Strip the prefix so every entry we toggle writes a row the composition
|
|
42
|
+
* actually matches; non-insert rows are unaffected.
|
|
43
|
+
*/
|
|
44
|
+
function patchRowId(entryId) {
|
|
45
|
+
return entryId.replace(/^include:/u, '');
|
|
46
|
+
}
|
|
36
47
|
/** Official bundle entry ids (timer/llm/session/… from dsh-base and
|
|
37
48
|
* dsh-web-app patch inserts): disabling these breaks the core chain, so
|
|
38
49
|
* they refuse to toggle alongside the pattern-based protection above. */
|
|
@@ -89,8 +100,11 @@ let toggleChain = Promise.resolve();
|
|
|
89
100
|
* @param disabled - the target stance.
|
|
90
101
|
* @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
|
|
91
102
|
*/
|
|
92
|
-
export function setDisabled(profileDir,
|
|
103
|
+
export function setDisabled(profileDir, entryId, disabled) {
|
|
93
104
|
const run = toggleChain.then(async () => {
|
|
105
|
+
// Loader runtime ids (include:<id>) never match patch composition —
|
|
106
|
+
// always address rows by their original patch id.
|
|
107
|
+
const id = patchRowId(entryId);
|
|
94
108
|
if (isProtected(id) || officialEntryIds(profileDir).has(id)) {
|
|
95
109
|
return { ok: false, detail: `"${id}" is host infrastructure and cannot be disabled`, nowDisabled: null };
|
|
96
110
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-plugin-center",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Plugin center for DeepSeek Harness
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Plugin center for DeepSeek Harness 閳?installed metadata, community market, update detection, and What\u0027s New",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -82,4 +82,4 @@
|
|
|
82
82
|
"react": "^18.2.0",
|
|
83
83
|
"typescript": "^5.5.0"
|
|
84
84
|
}
|
|
85
|
-
}
|
|
85
|
+
}
|