@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 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", 5e3);
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" : "\u{1F4F7}" }),
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.jsxs)("option", { value: "dsh-market", children: [
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 once, keeping the process-local cache. */
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 once (single PLUGINS.md parse). */
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 once (2BingLing/dsh-market, ~3900 plugins, trimmed). */
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 the dsh-market catalog (fetch or failure), with a hard deadline. */
120
- private waitDshMarket;
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 once, keeping the process-local cache. */
164
+ /** Start the awesome catalog fetch (with failed-retry cooldown). */
159
165
  prefetchAwesome() {
160
- if (this.awesomeFetching || this.awesomeDone)
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 { /* keep whatever cached so far */ }
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 once (single PLUGINS.md parse). */
221
+ /** Start the Oh-My-DSH fetch (single PLUGINS.md parse, with failed-retry cooldown). */
209
222
  prefetchOhMyDsh() {
210
- if (this.ohMyDshFetching || this.ohMyDshDone)
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 { /* empty on failure */ }
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 once (2BingLing/dsh-market, ~3900 plugins, trimmed). */
242
+ /** Start the dsh-market fetch (2BingLing/dsh-market, ~3900 plugins, trimmed, with failed-retry cooldown). */
223
243
  prefetchDshMarket() {
224
- if (this.dshMarketFetching || this.dshMarketDone)
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 { /* empty on failure */ }
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.ohMyDshDone && this.dshMarketDone,
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(() => installPlugin(spec, this.baseUrl));
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.waitDshMarket();
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
- const scored = this.dshMarketCache
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
- const hit = this.dshMarketCache.find(p => p.name === item.name || p.spec === item.name || p.npm === item.name);
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 the dsh-market catalog (fetch or failure), with a hard deadline. */
446
- async waitDshMarket() {
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(60000),
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, id: string, disabled: boolean): Promise<ToggleResult>;
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, id, disabled) {
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.1",
4
- "description": "Plugin center for DeepSeek Harness installed metadata, community market, update detection, and What's New",
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
+ }