@max-null/dsh-plugin-center 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -22,9 +22,9 @@ Plugin center for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-har
22
22
  ## Install / 安装
23
23
 
24
24
  ```sh
25
- dsh plugin --profile web add github:Max-Null/dsh-plugin-center
26
- # or, once published to npm / 或通过 npm(发布后)
27
25
  dsh plugin --profile web add @max-null/dsh-plugin-center
26
+ # or from the GitHub source / 或从 GitHub 源码安装
27
+ dsh plugin --profile web add github:Max-Null/dsh-plugin-center
28
28
  ```
29
29
 
30
30
  Restart `dsh web`, then open the plugin center from the header button (top-right) or the Settings → 插件中心 section.
package/client.js CHANGED
@@ -203,9 +203,143 @@ async function checkWhatNew() {
203
203
  } catch {
204
204
  }
205
205
  }
206
- var SOURCE_LABEL = { official: "\u5B98\u65B9", installed: "\u7528\u6237\u5B89\u88C5", local: "\u672C\u5730\u5F00\u53D1", builtin: "\u5185\u7F6E" };
207
206
  var CATEGORIES = ["ui", "usage", "theme", "model", "session", "memory", "tools", "vision", "skill", "workflow", "notify", "dev", "market", "fun"];
207
+ var STRINGS = {
208
+ zh: {
209
+ title: "\u63D2\u4EF6\u4E2D\u5FC3",
210
+ tabInstalled: "\u5DF2\u5B89\u88C5",
211
+ tabMarket: "\u5E02\u573A",
212
+ tabUpdates: "\u66F4\u65B0",
213
+ headSummary: "\u5DF2\u5B89\u88C5 {a} \xB7 \u6709\u66F4\u65B0 {b} \xB7 \u5931\u6548 {c}",
214
+ check: "\u68C0\u67E5\u66F4\u65B0",
215
+ checking: "\u68C0\u67E5\u4E2D\u2026",
216
+ updateAll: "\u66F4\u65B0\u5168\u90E8\uFF08{n}\uFF09",
217
+ searchInstalled: "\u641C\u7D22\u5DF2\u5B89\u88C5\u63D2\u4EF6",
218
+ allSources: "\u5168\u90E8\u6765\u6E90",
219
+ srcOfficial: "\u5B98\u65B9",
220
+ srcInstalled: "\u7528\u6237\u5B89\u88C5",
221
+ srcLocal: "\u672C\u5730\u5F00\u53D1",
222
+ srcBuiltin: "\u5185\u7F6E",
223
+ all: "\u5168\u90E8",
224
+ searchMarket: "\u641C\u7D22\u793E\u533A\u63D2\u4EF6",
225
+ allMarkets: "\u5168\u90E8\u6E90",
226
+ gridDouble: "\u53CC\u5217\u7F51\u683C",
227
+ gridSingle: "\u5355\u5217\u5217\u8868",
228
+ loadFailed: "\u52A0\u8F7D\u5931\u8D25\uFF1A{e}",
229
+ loading: "\u52A0\u8F7D\u4E2D\u2026",
230
+ marketLoading: "\u52A0\u8F7D\u5E02\u573A\u76EE\u5F55\u4E2D\u2026",
231
+ loadMore: "\u6B63\u5728\u52A0\u8F7D\u66F4\u591A\u2026\uFF08\u5DF2\u52A0\u8F7D {n} \u4E2A\uFF09",
232
+ checkingUpdates: "\u68C0\u67E5\u66F4\u65B0\u4E2D\u2026",
233
+ noUpdates: "\u6CA1\u6709\u53EF\u7528\u7684\u66F4\u65B0\u3002",
234
+ recheck: "\u91CD\u65B0\u68C0\u67E5",
235
+ incompat: "\u4E0D\u517C\u5BB9\u5F53\u524D DSH",
236
+ update: "\u66F4\u65B0",
237
+ updating: "\u66F4\u65B0\u4E2D\u2026",
238
+ install: "\u5B89\u88C5",
239
+ installing: "\u5B89\u88C5\u4E2D\u2026",
240
+ pendingRestart: "\u5F85\u91CD\u542F\u751F\u6548",
241
+ installedTag: "\u5DF2\u5B89\u88C5",
242
+ disabledTag: "\u5DF2\u7981\u7528",
243
+ requiresDsh: "\u8981\u6C42 DSH {r}",
244
+ installQueued: "\u5DF2\u53D1\u8D77\u5B89\u88C5 {n}\uFF0C\u91CD\u542F dsh web \u540E\u751F\u6548",
245
+ installFailed: "\u5B89\u88C5\u5931\u8D25\uFF1A{e}",
246
+ installNotApplied: "\u5B89\u88C5\u672A\u751F\u6548",
247
+ updatedOne: "\u5DF2\u66F4\u65B0 {n}\uFF0C\u91CD\u542F dsh web \u540E\u751F\u6548",
248
+ updateFailed: "\u66F4\u65B0\u5931\u8D25\uFF1A{e}",
249
+ updateNotApplied: "\u66F4\u65B0\u672A\u751F\u6548",
250
+ updatedMany: "\u5DF2\u66F4\u65B0 {n} \u4E2A\u63D2\u4EF6\uFF0C\u91CD\u542F dsh web \u540E\u751F\u6548",
251
+ updateSummary: "\u66F4\u65B0\u5B8C\u6210\uFF1A\u6210\u529F {a}\uFF0C\u5931\u8D25 {b}\uFF08{c}\uFF09",
252
+ whatsNewTitle: "\u63D2\u4EF6\u66F4\u65B0",
253
+ whatsNewSub: "{n} \u4E2A\u63D2\u4EF6\u6709\u65B0\u7248\u672C",
254
+ later: "\u7A0D\u540E",
255
+ markAllRead: "\u5168\u90E8\u6807\u8BB0\u5DF2\u8BFB",
256
+ updateNow: "\u7ACB\u5373\u66F4\u65B0",
257
+ close: "\u5173\u95ED",
258
+ checkFail: "\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
259
+ foundUpdates: "\u53D1\u73B0 {n} \u4E2A\u53EF\u66F4\u65B0\u63D2\u4EF6",
260
+ allUpToDate: "\u6240\u6709\u63D2\u4EF6\u5747\u4E3A\u6700\u65B0"
261
+ },
262
+ en: {
263
+ title: "Plugin Center",
264
+ tabInstalled: "Installed",
265
+ tabMarket: "Market",
266
+ tabUpdates: "Updates",
267
+ headSummary: "{a} installed \xB7 {b} updates \xB7 {c} failed",
268
+ check: "Check updates",
269
+ checking: "Checking\u2026",
270
+ updateAll: "Update all\uFF08{n}\uFF09",
271
+ searchInstalled: "Search installed plugins",
272
+ allSources: "All sources",
273
+ srcOfficial: "Official",
274
+ srcInstalled: "User installed",
275
+ srcLocal: "Local dev",
276
+ srcBuiltin: "Built-in",
277
+ all: "All",
278
+ searchMarket: "Search community plugins",
279
+ allMarkets: "All sources",
280
+ gridDouble: "Two-column grid",
281
+ gridSingle: "Single-column list",
282
+ loadFailed: "Failed to load: {e}",
283
+ loading: "Loading\u2026",
284
+ marketLoading: "Loading market catalog\u2026",
285
+ loadMore: "Loading more\u2026 ({n} loaded)",
286
+ checkingUpdates: "Checking for updates\u2026",
287
+ noUpdates: "No updates available.",
288
+ recheck: "Check again",
289
+ incompat: "Incompatible with current DSH",
290
+ update: "Update",
291
+ updating: "Updating\u2026",
292
+ install: "Install",
293
+ installing: "Installing\u2026",
294
+ pendingRestart: "Restart pending",
295
+ installedTag: "Installed",
296
+ disabledTag: "Disabled",
297
+ requiresDsh: "Requires DSH {r}",
298
+ installQueued: "Install of {n} started; restart dsh web to take effect",
299
+ installFailed: "Install failed: {e}",
300
+ installNotApplied: "Install did not take effect",
301
+ updatedOne: "Updated {n}; restart dsh web to take effect",
302
+ updateFailed: "Update failed: {e}",
303
+ updateNotApplied: "Update did not take effect",
304
+ updatedMany: "Updated {n} plugins; restart dsh web to take effect",
305
+ updateSummary: "Update done: {a} succeeded, {b} failed ({c})",
306
+ whatsNewTitle: "Plugin updates",
307
+ whatsNewSub: "{n} plugins have new versions",
308
+ later: "Later",
309
+ markAllRead: "Mark all read",
310
+ updateNow: "Update now",
311
+ close: "Close",
312
+ checkFail: "Failed to check updates, please retry later",
313
+ foundUpdates: "{n} updates found",
314
+ allUpToDate: "All plugins are up to date"
315
+ }
316
+ };
317
+ var localeId = "zh";
318
+ var localeListeners = /* @__PURE__ */ new Set();
319
+ function adoptLocale(id) {
320
+ const next = id === "en" ? "en" : "zh";
321
+ if (next === localeId) return;
322
+ localeId = next;
323
+ localeListeners.forEach((l) => l());
324
+ }
325
+ function fmt(tpl, vars = {}) {
326
+ return tpl.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? ""));
327
+ }
328
+ function useT() {
329
+ const [id, setId] = (0, import_react.useState)(localeId);
330
+ (0, import_react.useEffect)(() => {
331
+ const l = () => {
332
+ setId(localeId);
333
+ };
334
+ localeListeners.add(l);
335
+ return () => {
336
+ localeListeners.delete(l);
337
+ };
338
+ }, []);
339
+ return (key, vars) => fmt(STRINGS[id][key] ?? STRINGS.zh[key], vars);
340
+ }
208
341
  function InstalledView({ search, category, source }) {
342
+ const t = useT();
209
343
  const [items, setItems] = (0, import_react.useState)(installedCache);
210
344
  const [error, setError] = (0, import_react.useState)(null);
211
345
  (0, import_react.useEffect)(() => {
@@ -224,11 +358,14 @@ function InstalledView({ search, category, source }) {
224
358
  alive = false;
225
359
  };
226
360
  }, []);
227
- if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "pc-sub", children: [
228
- "\u52A0\u8F7D\u5931\u8D25\uFF1A",
229
- error
230
- ] });
231
- if (items === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: "\u52A0\u8F7D\u4E2D\u2026" });
361
+ if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: t("loadFailed", { e: error }) });
362
+ if (items === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: t("loading") });
363
+ const srcLabel = {
364
+ official: t("srcOfficial"),
365
+ installed: t("srcInstalled"),
366
+ local: t("srcLocal"),
367
+ builtin: t("srcBuiltin")
368
+ };
232
369
  const q = search.trim().toLowerCase();
233
370
  const filtered = items.filter((p) => {
234
371
  const matchSearch = q === "" || p.displayName.toLowerCase().includes(q) || (p.description ?? "").toLowerCase().includes(q) || p.name.toLowerCase().includes(q);
@@ -243,22 +380,20 @@ function InstalledView({ search, category, source }) {
243
380
  "v",
244
381
  p.version
245
382
  ] }),
246
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `pc-badge ${p.source}`, children: SOURCE_LABEL[p.source] }),
383
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `pc-badge ${p.source}`, children: srcLabel[p.source] }),
247
384
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
248
385
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `pc-dot${p.fiberPhase === "failed" ? " failed" : ""}` })
249
386
  ] }),
250
387
  p.description !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-desc", children: p.description }),
251
388
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-meta", children: [
252
389
  p.categories.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: c }, c)),
253
- p.compatRange !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "pc-tag", children: [
254
- "\u8981\u6C42 DSH ",
255
- p.compatRange
256
- ] }),
257
- !p.enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: "\u5DF2\u7981\u7528" })
390
+ p.compatRange !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: t("requiresDsh", { r: p.compatRange }) }),
391
+ !p.enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: t("disabledTag") })
258
392
  ] })
259
393
  ] }, p.entryId)) });
260
394
  }
261
395
  function MarketView({ category, single, source, search, onCount }) {
396
+ const t = useT();
262
397
  const [items, setItems] = (0, import_react.useState)(marketCache[source]?.plugins ?? []);
263
398
  const [done, setDone] = (0, import_react.useState)(marketCache[source]?.done ?? false);
264
399
  const [error, setError] = (0, import_react.useState)(null);
@@ -296,20 +431,19 @@ function MarketView({ category, single, source, search, onCount }) {
296
431
  if (timer !== void 0) clearTimeout(timer);
297
432
  };
298
433
  }, [source, onCount]);
299
- if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "pc-sub", children: [
300
- "\u52A0\u8F7D\u5931\u8D25\uFF1A",
301
- error
302
- ] });
303
- if (items.length === 0 && !done) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: "\u52A0\u8F7D\u5E02\u573A\u76EE\u5F55\u4E2D\u2026" });
434
+ if (error !== null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: t("loadFailed", { e: error }) });
435
+ if (items.length === 0 && !done) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: t("marketLoading") });
304
436
  const q = search.trim().toLowerCase();
305
437
  const filtered = items.filter((m) => {
306
438
  const matchSearch = q === "" || m.name.toLowerCase().includes(q) || (m.description.zh || m.description.en).toLowerCase().includes(q);
307
439
  return matchSearch && (category === null || m.categories.includes(category));
308
440
  });
441
+ const descOf = (m) => localeId === "en" ? m.description.en || m.description.zh : m.description.zh || m.description.en;
309
442
  const install = (m) => {
310
443
  setBusy(m.name);
311
444
  void rpc("install", { spec: m.spec }).then(
312
- () => {
445
+ (v) => {
446
+ if (v !== true) throw new Error(t("installNotApplied"));
313
447
  setBusy(null);
314
448
  pendingInstall.add(m.spec);
315
449
  for (const key of Object.keys(marketCache)) {
@@ -317,11 +451,11 @@ function MarketView({ category, single, source, search, onCount }) {
317
451
  if (c !== void 0) c.plugins = c.plugins.map((p) => p.name === m.name ? { ...p, installed: true } : p);
318
452
  }
319
453
  setItems((prev) => prev.map((p) => p.name === m.name ? { ...p, installed: true } : p));
320
- showToast(`\u5DF2\u53D1\u8D77\u5B89\u88C5 ${m.name}\uFF0C\u91CD\u542F dsh web \u540E\u751F\u6548`);
454
+ showToast(t("installQueued", { n: m.name }));
321
455
  },
322
456
  (e) => {
323
457
  setBusy(null);
324
- showToast(`\u5B89\u88C5\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`, "error");
458
+ showToast(t("installFailed", { e: e instanceof Error ? e.message : String(e) }), "error");
325
459
  }
326
460
  );
327
461
  };
@@ -338,27 +472,24 @@ function MarketView({ category, single, source, search, onCount }) {
338
472
  m.version
339
473
  ] })
340
474
  ] }),
341
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-desc", children: m.description.zh || m.description.en }),
475
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-desc", children: descOf(m) }),
342
476
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-meta", children: [
343
477
  m.categories.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: c }, c)),
344
478
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
345
- m.installed || pendingInstall.has(m.spec) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: pendingInstall.has(m.spec) ? "\u5F85\u91CD\u542F\u751F\u6548" : "\u5DF2\u5B89\u88C5" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: busy !== null, onClick: () => {
479
+ 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: () => {
346
480
  install(m);
347
- }, children: busy === m.name ? "\u5B89\u88C5\u4E2D\u2026" : "\u5B89\u88C5" })
481
+ }, children: busy === m.name ? t("installing") : t("install") })
348
482
  ] })
349
483
  ] }, m.name)) }),
350
- !done && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "pc-sub", children: [
351
- "\u6B63\u5728\u52A0\u8F7D\u66F4\u591A\u2026\uFF08\u5DF2\u52A0\u8F7D ",
352
- items.length,
353
- " \u4E2A\uFF09"
354
- ] })
484
+ !done && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: t("loadMore", { n: items.length }) })
355
485
  ] });
356
486
  }
357
487
  function UpdatesView({ updates, refresh, updateOne, busy }) {
358
- if (updates === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: "\u68C0\u67E5\u66F4\u65B0\u4E2D\u2026" });
488
+ const t = useT();
489
+ if (updates === null) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: t("checkingUpdates") });
359
490
  if (updates.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
360
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: "\u6CA1\u6709\u53EF\u7528\u7684\u66F4\u65B0\u3002" }),
361
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", onClick: refresh, children: "\u91CD\u65B0\u68C0\u67E5" })
491
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", children: t("noUpdates") }),
492
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", onClick: refresh, children: t("recheck") })
362
493
  ] });
363
494
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: updates.map((u) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-card", children: [
364
495
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-row", children: [
@@ -366,16 +497,17 @@ function UpdatesView({ updates, refresh, updateOne, busy }) {
366
497
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-ver", children: u.fromVersion }),
367
498
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-ver", children: "\u2192" }),
368
499
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: "var(--dsw-alias-state-business-primary)", fontWeight: 500 }, children: u.toVersion }),
369
- u.compat === "incompatible" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag danger", children: "\u4E0D\u517C\u5BB9\u5F53\u524D DSH" }),
500
+ u.compat === "incompatible" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag danger", children: t("incompat") }),
370
501
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
371
502
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: busy !== null, onClick: () => {
372
503
  updateOne(u.name);
373
- }, children: busy === u.name ? "\u66F4\u65B0\u4E2D\u2026" : "\u66F4\u65B0" })
504
+ }, children: busy === u.name || busy === "__all__" ? t("updating") : t("update") })
374
505
  ] }),
375
506
  u.changelog.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "pc-wn-list", children: u.changelog.slice(0, 5).map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: line }, i)) })
376
507
  ] }, u.name)) });
377
508
  }
378
509
  function CenterPanel({ variant = "section" }) {
510
+ const t = useT();
379
511
  const [view, setView] = (0, import_react.useState)("installed");
380
512
  const [category, setCategory] = (0, import_react.useState)(null);
381
513
  const [single, setSingle] = (0, import_react.useState)(false);
@@ -389,15 +521,24 @@ function CenterPanel({ variant = "section" }) {
389
521
  const [marketCount, setMarketCount] = (0, import_react.useState)(0);
390
522
  const [updates, setUpdates] = (0, import_react.useState)(null);
391
523
  const [busyUpdate, setBusyUpdate] = (0, import_react.useState)(null);
392
- const refreshUpdates = (0, import_react.useCallback)(() => {
524
+ const [checking, setChecking] = (0, import_react.useState)(false);
525
+ const refreshUpdates = (0, import_react.useCallback)((silent = false) => {
526
+ setChecking(true);
393
527
  void rpc("checkUpdates", { since: new Date(Date.now() - 30 * 864e5).toISOString() }).then(
394
528
  (v) => {
395
- setUpdates(v);
529
+ const digests = v;
530
+ setUpdates(digests);
531
+ setChecking(false);
532
+ if (!silent) {
533
+ showToast(digests.length > 0 ? t("foundUpdates", { n: digests.length }) : t("allUpToDate"));
534
+ }
396
535
  },
397
536
  () => {
537
+ setChecking(false);
538
+ if (!silent) showToast(t("checkFail"), "error");
398
539
  }
399
540
  );
400
- }, []);
541
+ }, [t]);
401
542
  (0, import_react.useEffect)(() => {
402
543
  void rpc("listInstalled").then(
403
544
  (v) => {
@@ -408,27 +549,68 @@ function CenterPanel({ variant = "section" }) {
408
549
  () => {
409
550
  }
410
551
  );
411
- refreshUpdates();
552
+ refreshUpdates(true);
412
553
  }, [refreshUpdates]);
554
+ (0, import_react.useEffect)(() => {
555
+ let alive = true;
556
+ let timer = null;
557
+ const poll = async () => {
558
+ if (!alive) return;
559
+ try {
560
+ const r = await rpc("listMarket", { source: "all" });
561
+ if (!alive) return;
562
+ marketCache.all = { plugins: r.plugins, done: r.done };
563
+ setMarketCount(r.plugins.length);
564
+ if (!r.done) timer = setTimeout(() => {
565
+ void poll();
566
+ }, 5e3);
567
+ } catch {
568
+ if (alive) timer = setTimeout(() => {
569
+ void poll();
570
+ }, 15e3);
571
+ }
572
+ };
573
+ void poll();
574
+ return () => {
575
+ alive = false;
576
+ if (timer !== null) clearTimeout(timer);
577
+ };
578
+ }, []);
413
579
  const updateOne = (name) => {
414
580
  setBusyUpdate(name);
415
581
  void rpc("update", { name }).then(
416
- () => {
582
+ (v) => {
583
+ if (v !== true) throw new Error(t("updateNotApplied"));
417
584
  setBusyUpdate(null);
418
- showToast(`\u5DF2\u66F4\u65B0 ${name}\uFF0C\u91CD\u542F dsh web \u540E\u751F\u6548`);
585
+ showToast(t("updatedOne", { n: name }));
419
586
  },
420
587
  (e) => {
421
588
  setBusyUpdate(null);
422
- showToast(`\u66F4\u65B0\u5931\u8D25\uFF1A${e instanceof Error ? e.message : String(e)}`, "error");
589
+ showToast(t("updateFailed", { e: e instanceof Error ? e.message : String(e) }), "error");
423
590
  }
424
591
  );
425
592
  };
426
- const updateAll = () => {
427
- if (updates !== null) {
428
- for (const u of updates) {
429
- void updateOne(u.name);
593
+ const updateAll = async () => {
594
+ if (updates === null || updates.length === 0) return;
595
+ setBusyUpdate("__all__");
596
+ let okCount = 0;
597
+ const failures = [];
598
+ for (const u of updates) {
599
+ try {
600
+ const v = await rpc("update", { name: u.name });
601
+ if (v !== true) throw new Error(t("updateNotApplied"));
602
+ okCount++;
603
+ } catch (e) {
604
+ failures.push(`${u.name}\uFF1A${e instanceof Error ? e.message : String(e)}`);
430
605
  }
431
606
  }
607
+ setBusyUpdate(null);
608
+ if (failures.length === 0) {
609
+ showToast(t("updatedMany", { n: okCount }));
610
+ } else {
611
+ showToast(t("updateSummary", { a: okCount, b: failures.length, c: failures.join("\uFF1B") }), "error");
612
+ }
613
+ refreshUpdates();
432
614
  };
433
615
  const tab = (v, label, count) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: `pc-tab${view === v ? " active" : ""}`, onClick: () => {
434
616
  setView(v);
@@ -437,44 +619,37 @@ function CenterPanel({ variant = "section" }) {
437
619
  count !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-count", children: count })
438
620
  ] }, v);
439
621
  const tabs = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-tabs", children: [
440
- tab("installed", "\u5DF2\u5B89\u88C5", installedCount),
441
- tab("market", "\u5E02\u573A", marketCount),
442
- tab("updates", "\u66F4\u65B0", updates?.length ?? 0)
622
+ tab("installed", t("tabInstalled"), installedCount),
623
+ tab("market", t("tabMarket"), marketCount),
624
+ tab("updates", t("tabUpdates"), updates?.length ?? 0)
443
625
  ] });
444
626
  const head = (showTitle) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-head", children: [
445
- showTitle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-title", children: "\u63D2\u4EF6\u4E2D\u5FC3" }),
446
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "pc-sub", children: [
447
- "\u5DF2\u5B89\u88C5 ",
448
- installedCount,
449
- " \xB7 \u6709\u66F4\u65B0 ",
450
- updates?.length ?? 0,
451
- " \xB7 \u5931\u6548 ",
452
- failedCount
453
- ] }),
627
+ showTitle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-title", children: t("title") }),
628
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-sub", children: t("headSummary", { a: installedCount, b: updates?.length ?? 0, c: failedCount }) }),
454
629
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
455
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", onClick: refreshUpdates, children: "\u68C0\u67E5\u66F4\u65B0" }),
456
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: "pc-btn primary", disabled: !updates?.length, onClick: updateAll, children: [
457
- "Update All\uFF08",
458
- updates?.length ?? 0,
459
- "\uFF09"
460
- ] })
630
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", disabled: checking, onClick: () => {
631
+ refreshUpdates();
632
+ }, children: checking ? t("checking") : t("check") }),
633
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: !updates?.length || busyUpdate !== null, onClick: () => {
634
+ void updateAll();
635
+ }, children: t("updateAll", { n: updates?.length ?? 0 }) })
461
636
  ] });
462
637
  const installedToolbar = view === "installed" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-toolbar", children: [
463
638
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "pc-search", value: search, onChange: (e) => {
464
639
  setSearch(e.target.value);
465
- }, placeholder: "\u641C\u7D22\u5DF2\u5B89\u88C5\u63D2\u4EF6" }),
640
+ }, placeholder: t("searchInstalled") }),
466
641
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { className: "pc-select", value: installedSource ?? "", onChange: (e) => {
467
642
  setInstalledSource(e.target.value === "" ? null : e.target.value);
468
643
  }, children: [
469
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "\u5168\u90E8\u6765\u6E90" }),
470
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "official", children: "\u5B98\u65B9" }),
471
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "installed", children: "\u7528\u6237\u5B89\u88C5" }),
472
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "local", children: "\u672C\u5730\u5F00\u53D1" }),
473
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "builtin", children: "\u5185\u7F6E" })
644
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: t("allSources") }),
645
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "official", children: t("srcOfficial") }),
646
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "installed", children: t("srcInstalled") }),
647
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "local", children: t("srcLocal") }),
648
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "builtin", children: t("srcBuiltin") })
474
649
  ] }),
475
650
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: `pc-chip${installedCategory === null ? " active" : ""}`, onClick: () => {
476
651
  setInstalledCategory(null);
477
- }, children: "\u5168\u90E8" }),
652
+ }, children: t("all") }),
478
653
  CATEGORIES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: `pc-chip${installedCategory === c ? " active" : ""}`, onClick: () => {
479
654
  setInstalledCategory(c);
480
655
  }, children: c }, c))
@@ -482,23 +657,23 @@ function CenterPanel({ variant = "section" }) {
482
657
  const marketToolbar = view === "market" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-toolbar", children: [
483
658
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "pc-search", value: marketSearch, onChange: (e) => {
484
659
  setMarketSearch(e.target.value);
485
- }, placeholder: "\u641C\u7D22\u793E\u533A\u63D2\u4EF6" }),
660
+ }, placeholder: t("searchMarket") }),
486
661
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { className: "pc-select", value: source, onChange: (e) => {
487
662
  setSource(e.target.value);
488
663
  setCategory(null);
489
664
  }, children: [
490
665
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "awesome", children: "awesome-dsh-plugin" }),
491
666
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "oh-my-dsh", children: "Oh-My-DSH" }),
492
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: "\u5168\u90E8\u6E90" })
667
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "all", children: t("allMarkets") })
493
668
  ] }),
494
669
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: `pc-chip${category === null ? " active" : ""}`, onClick: () => {
495
670
  setCategory(null);
496
- }, children: "\u5168\u90E8" }),
671
+ }, children: t("all") }),
497
672
  CATEGORIES.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: `pc-chip${category === c ? " active" : ""}`, onClick: () => {
498
673
  setCategory(c);
499
674
  }, children: c }, c)),
500
675
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
501
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", title: single ? "\u53CC\u5217\u7F51\u683C" : "\u5355\u5217\u5217\u8868", "aria-label": "\u5207\u6362\u5E03\u5C40", className: "pc-iconbtn", onClick: () => {
676
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", title: single ? t("gridDouble") : t("gridSingle"), "aria-label": single ? t("gridDouble") : t("gridSingle"), className: "pc-iconbtn", onClick: () => {
502
677
  setSingle((v) => !v);
503
678
  }, children: single ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
504
679
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "5", height: "5", rx: "1" }),
@@ -537,7 +712,8 @@ function CenterPanel({ variant = "section" }) {
537
712
  ] });
538
713
  }
539
714
  function HeaderButton() {
540
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", title: "\u63D2\u4EF6\u4E2D\u5FC3", "aria-label": "\u63D2\u4EF6\u4E2D\u5FC3", className: "pc-headerbtn", onClick: openOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
715
+ const t = useT();
716
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", title: t("title"), "aria-label": t("title"), className: "pc-headerbtn", onClick: openOverlay, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [
541
717
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "2", width: "5", height: "5", rx: "1" }),
542
718
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "9", y: "2", width: "5", height: "5", rx: "1" }),
543
719
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "9", width: "5", height: "5", rx: "1" }),
@@ -545,13 +721,14 @@ function HeaderButton() {
545
721
  ] }) });
546
722
  }
547
723
  function OverlayPanel() {
724
+ const t = useT();
548
725
  const open = useOverlayOpen();
549
726
  if (!open) return null;
550
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-overlay", role: "presentation", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel", role: "dialog", "aria-modal": "true", "aria-label": "\u63D2\u4EF6\u4E2D\u5FC3", children: [
727
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-overlay", role: "presentation", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel", role: "dialog", "aria-modal": "true", "aria-label": t("title"), children: [
551
728
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel-head", children: [
552
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-title", children: "\u63D2\u4EF6\u4E2D\u5FC3" }),
729
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-title", children: t("title") }),
553
730
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
554
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "pc-close", onClick: closeOverlay, "aria-label": "\u5173\u95ED", children: "\u2715" })
731
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "pc-close", onClick: closeOverlay, "aria-label": t("close"), children: "\u2715" })
555
732
  ] }),
556
733
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-panel-body", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CenterPanel, { variant: "overlay" }) })
557
734
  ] }) });
@@ -562,17 +739,37 @@ function Toast() {
562
739
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `pc-toast ${t.kind}`, children: t.message });
563
740
  }
564
741
  function WhatsNewDialog() {
742
+ const t = useT();
565
743
  const open = useWhatsNewOpen();
744
+ const [busy, setBusy] = (0, import_react.useState)(false);
566
745
  if (!open || whatsNewDigests.length === 0) return null;
567
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-overlay", role: "presentation", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel", style: { width: "540px" }, role: "dialog", "aria-modal": "true", "aria-label": "\u63D2\u4EF6\u66F4\u65B0", children: [
746
+ const updateNow = async () => {
747
+ setBusy(true);
748
+ let okCount = 0;
749
+ const failures = [];
750
+ for (const u of whatsNewDigests) {
751
+ try {
752
+ const v = await rpc("update", { name: u.name });
753
+ if (v !== true) throw new Error(t("updateNotApplied"));
754
+ okCount++;
755
+ } catch (e) {
756
+ failures.push(`${u.name}\uFF1A${e instanceof Error ? e.message : String(e)}`);
757
+ }
758
+ }
759
+ setBusy(false);
760
+ if (failures.length === 0) {
761
+ showToast(t("updatedMany", { n: okCount }));
762
+ closeWhatsNew();
763
+ } else {
764
+ showToast(t("updateSummary", { a: okCount, b: failures.length, c: failures.join("\uFF1B") }), "error");
765
+ }
766
+ };
767
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-overlay", role: "presentation", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel", style: { width: "540px" }, role: "dialog", "aria-modal": "true", "aria-label": t("whatsNewTitle"), children: [
568
768
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel-head", children: [
569
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-title", children: "\u63D2\u4EF6\u66F4\u65B0" }),
570
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "pc-sub", style: { marginTop: 0 }, children: [
571
- whatsNewDigests.length,
572
- " \u4E2A\u63D2\u4EF6\u6709\u65B0\u7248\u672C"
573
- ] }),
769
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-title", children: t("whatsNewTitle") }),
770
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-sub", style: { marginTop: 0 }, children: t("whatsNewSub", { n: whatsNewDigests.length }) }),
574
771
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
575
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "pc-close", onClick: closeWhatsNew, "aria-label": "\u5173\u95ED", children: "\u2715" })
772
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "pc-close", onClick: closeWhatsNew, "aria-label": t("close"), children: "\u2715" })
576
773
  ] }),
577
774
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel-body", style: { overflow: "auto" }, children: [
578
775
  whatsNewDigests.map((u) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-wn-item", children: [
@@ -585,8 +782,11 @@ function WhatsNewDialog() {
585
782
  u.changelog.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "pc-wn-list", children: u.changelog.slice(0, 5).map((line, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: line }, i)) })
586
783
  ] }, u.name)),
587
784
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "16px" }, children: [
588
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", onClick: closeWhatsNew, children: "\u7A0D\u540E" }),
589
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", onClick: closeWhatsNew, children: "\u5168\u90E8\u6807\u8BB0\u5DF2\u8BFB" })
785
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", onClick: closeWhatsNew, children: t("later") }),
786
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", onClick: closeWhatsNew, children: t("markAllRead") }),
787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: busy, onClick: () => {
788
+ void updateNow();
789
+ }, children: busy ? t("updating") : t("updateNow") })
590
790
  ] })
591
791
  ] })
592
792
  ] }) });
@@ -599,11 +799,16 @@ function apply(ctx) {
599
799
  if (result.ok) return result.value;
600
800
  throw new Error(result.error?.message ?? `plugin-center: ${endpoint} failed`);
601
801
  };
802
+ const initial = ctx.locale?.getLocale?.()?.active;
803
+ if (typeof initial === "string") adoptLocale(initial);
804
+ ctx.on?.("locale/change", (snap) => {
805
+ adoptLocale(snap?.active);
806
+ });
602
807
  ctx.slots.inject("settings.section", () => ctx.slots.register({
603
808
  name: "settings.section",
604
809
  id: "plugin-center",
605
810
  order: 50,
606
- label: () => "\u63D2\u4EF6\u4E2D\u5FC3"
811
+ label: () => STRINGS[localeId].title
607
812
  }, CenterPanel));
608
813
  ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
609
814
  name: "conversation.session.header.utilities",
package/dist/engine.d.ts CHANGED
@@ -33,6 +33,8 @@ export declare class PluginCenterEngine extends Service {
33
33
  private installedNamesCache;
34
34
  private updatesCache;
35
35
  private readonly updatesTtlMs;
36
+ /** 串行链:同 profile 的 pnpm 调用禁止并发(并发 add 会撞 store 锁/写坏 lock)。 */
37
+ private pnpmChain;
36
38
  constructor(ctx: Context);
37
39
  /** Background preload of market + installed metadata; failures fall back to lazy load. */
38
40
  private warmup;
@@ -63,8 +65,16 @@ export declare class PluginCenterEngine extends Service {
63
65
  listMarket(source?: MarketSource): Promise<MarketSnapshot>;
64
66
  /** Detect updates for every installed third-party/local plugin, TTL-cached. */
65
67
  checkUpdates(sinceIso: string): Promise<UpdateDigest[]>;
66
- install(spec: string): Promise<boolean>;
67
- update(name: string): Promise<boolean>;
68
+ install(spec: string): Promise<{
69
+ ok: boolean;
70
+ detail: string;
71
+ }>;
72
+ update(name: string): Promise<{
73
+ ok: boolean;
74
+ detail: string;
75
+ }>;
76
+ /** 串行执行一次 pnpm 操作并失效缓存(无论成败都放行链条后续任务)。 */
77
+ private enqueuePnpm;
68
78
  /** Temporary diagnostics for the empty-update bug; removed once root-caused. */
69
79
  debug(): Promise<{
70
80
  baseUrl: string;
package/dist/engine.js CHANGED
@@ -34,6 +34,8 @@ export class PluginCenterEngine extends Service {
34
34
  installedNamesCache = null;
35
35
  updatesCache = null;
36
36
  updatesTtlMs = 5 * 60_000;
37
+ /** 串行链:同 profile 的 pnpm 调用禁止并发(并发 add 会撞 store 锁/写坏 lock)。 */
38
+ pnpmChain = Promise.resolve();
37
39
  constructor(ctx) {
38
40
  super(ctx, 'pluginCenter');
39
41
  // Warm caches in the background once the host is up, so opening the panel
@@ -221,16 +223,21 @@ export class PluginCenterEngine extends Service {
221
223
  return this.updatesCache.digests;
222
224
  }
223
225
  async install(spec) {
224
- const ok = await installPlugin(spec, this.baseUrl);
225
- this.installedNamesCache = null;
226
- this.updatesCache = null;
227
- return ok;
226
+ return this.enqueuePnpm(() => installPlugin(spec, this.baseUrl));
228
227
  }
229
228
  async update(name) {
230
- const ok = await updatePlugin(name, this.baseUrl);
231
- this.installedNamesCache = null;
232
- this.updatesCache = null;
233
- return ok;
229
+ return this.enqueuePnpm(() => updatePlugin(name, this.baseUrl));
230
+ }
231
+ /** 串行执行一次 pnpm 操作并失效缓存(无论成败都放行链条后续任务)。 */
232
+ enqueuePnpm(op) {
233
+ const run = this.pnpmChain.then(async () => {
234
+ const result = await op();
235
+ this.installedNamesCache = null;
236
+ this.updatesCache = null;
237
+ return result;
238
+ });
239
+ this.pnpmChain = run.catch(() => { });
240
+ return run;
234
241
  }
235
242
  /** Temporary diagnostics for the empty-update bug; removed once root-caused. */
236
243
  async debug() {
package/dist/rpc.js CHANGED
@@ -28,13 +28,19 @@ export class PluginCenterRpc extends Service {
28
28
  const spec = payload?.spec;
29
29
  if (typeof spec !== 'string' || spec === '')
30
30
  return internal('install: spec is required');
31
- return { ok: true, value: await ctx.pluginCenter.install(spec) };
31
+ const result = await ctx.pluginCenter.install(spec);
32
+ if (!result.ok)
33
+ return internal(`install ${spec} 失败:${result.detail}`);
34
+ return { ok: true, value: true };
32
35
  }
33
36
  case 'update': {
34
37
  const name = payload?.name;
35
38
  if (typeof name !== 'string' || name === '')
36
39
  return internal('update: name is required');
37
- return { ok: true, value: await ctx.pluginCenter.update(name) };
40
+ const result = await ctx.pluginCenter.update(name);
41
+ if (!result.ok)
42
+ return internal(`update ${name} 失败:${result.detail}`);
43
+ return { ok: true, value: true };
38
44
  }
39
45
  case 'debug':
40
46
  return { ok: true, value: await ctx.pluginCenter.debug() };
package/dist/update.d.ts CHANGED
@@ -17,14 +17,22 @@ export declare function fetchCommitChangelog(repoUrl: string | null, sinceIso: s
17
17
  * DSH version via the plugin's peer-dependency range.
18
18
  */
19
19
  export declare function detectUpdate(name: string, localVersion: string, repoUrl: string | null, compatRange: string | null, localDshVersion: string, sinceIso: string): Promise<UpdateDigest | null>;
20
+ /** One pnpm invocation result: success plus a failure detail for diagnostics. */
21
+ export interface PnpmResult {
22
+ ok: boolean;
23
+ detail: string;
24
+ }
25
+ /** pnpm 候选命令:GUI 进程 PATH 常缺用户级 npm 全局目录;且 Windows 上
26
+ * CreateProcess 只找 pnpm.exe(.cmd/.ps1 必须经 shell)——2026-08-17 实测
27
+ * spawn('pnpm', shell:false) 直接 ENOENT,更新永远假成功。 */
28
+ export declare function pnpmCandidates(): string[];
20
29
  /**
21
- * Run pnpm in the profile directory. Output inherits the process stdio (no
22
- * pipe capture the host sandbox forbids named-pipe stdio); the exit code is
23
- * the only result this layer needs.
24
- * @returns the child exit code, or 1 when spawn itself fails.
30
+ * Run pnpm in the profile directory, trying each candidate command in turn.
31
+ * Output inherits the process stdio (no pipe capture the host sandbox
32
+ * forbids named-pipe stdio); the exit code is the only result this layer needs.
25
33
  */
26
- export declare function runPnpm(args: readonly string[], cwd: string): Promise<number>;
34
+ export declare function runPnpm(args: readonly string[], cwd: string): Promise<PnpmResult>;
27
35
  /** Install a package into the web profile, mirroring `dsh plugin add` semantics. */
28
- export declare function installPlugin(packageSpec: string, profileDir: string): Promise<boolean>;
36
+ export declare function installPlugin(packageSpec: string, profileDir: string): Promise<PnpmResult>;
29
37
  /** Update a package to latest, mirroring `dsh plugin add <pkg>` (pnpm installs latest). */
30
- export declare function updatePlugin(packageName: string, profileDir: string): Promise<boolean>;
38
+ export declare function updatePlugin(packageName: string, profileDir: string): Promise<PnpmResult>;
package/dist/update.js CHANGED
@@ -4,6 +4,9 @@
4
4
  * first (many community repos ship no release/tag/CHANGELOG — verified §7.2).
5
5
  */
6
6
  import { spawn } from 'node:child_process';
7
+ import { existsSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join } from 'node:path';
7
10
  import { compareVersions, satisfies } from "./semver.js";
8
11
  const UA = { 'User-Agent': 'dsh-plugin-center' };
9
12
  /** Latest published version on the npm registry; null when unreachable/unpublished. */
@@ -67,34 +70,54 @@ export async function detectUpdate(name, localVersion, repoUrl, compatRange, loc
67
70
  compatRange,
68
71
  };
69
72
  }
70
- /**
71
- * Run pnpm in the profile directory. Output inherits the process stdio (no
72
- * pipe capture — the host sandbox forbids named-pipe stdio); the exit code is
73
- * the only result this layer needs.
74
- * @returns the child exit code, or 1 when spawn itself fails.
75
- */
76
- export function runPnpm(args, cwd) {
73
+ /** pnpm 候选命令:GUI 进程 PATH 常缺用户级 npm 全局目录;且 Windows 上
74
+ * CreateProcess 只找 pnpm.exe(.cmd/.ps1 必须经 shell)——2026-08-17 实测
75
+ * spawn('pnpm', shell:false) 直接 ENOENT,更新永远假成功。 */
76
+ export function pnpmCandidates() {
77
+ const commands = ['pnpm', 'pnpm.cmd'];
78
+ const userNpm = join(homedir(), 'AppData', 'Roaming', 'npm');
79
+ for (const name of ['pnpm.cmd', 'pnpm.exe', 'pnpm']) {
80
+ const candidate = join(userNpm, name);
81
+ if (existsSync(candidate))
82
+ commands.push(candidate);
83
+ }
84
+ return commands;
85
+ }
86
+ function runOne(command, args, cwd) {
77
87
  return new Promise((resolve) => {
78
88
  let child;
79
89
  try {
80
- child = spawn('pnpm', [...args], { cwd, stdio: 'inherit', shell: false });
90
+ // shell: true —— Windows .cmd shim 必须经 shell 才能 spawn。
91
+ child = spawn(command, [...args], { cwd, stdio: 'inherit', shell: true });
81
92
  }
82
- catch {
83
- resolve(1);
93
+ catch (e) {
94
+ resolve({ ok: false, detail: e instanceof Error ? e.message : String(e) });
84
95
  return;
85
96
  }
86
- child.on('error', () => resolve(1));
87
- child.on('close', code => resolve(code ?? 1));
97
+ child.on('error', e => resolve({ ok: false, detail: e.message }));
98
+ child.on('close', code => resolve(code === 0 ? { ok: true, detail: '' } : { ok: false, detail: `exit code ${code ?? 1}` }));
88
99
  });
89
100
  }
101
+ /**
102
+ * Run pnpm in the profile directory, trying each candidate command in turn.
103
+ * Output inherits the process stdio (no pipe capture — the host sandbox
104
+ * forbids named-pipe stdio); the exit code is the only result this layer needs.
105
+ */
106
+ export async function runPnpm(args, cwd) {
107
+ let last = { ok: false, detail: 'no pnpm candidate found' };
108
+ for (const command of pnpmCandidates()) {
109
+ last = await runOne(command, args, cwd);
110
+ if (last.ok)
111
+ return last;
112
+ }
113
+ return last;
114
+ }
90
115
  /** Install a package into the web profile, mirroring `dsh plugin add` semantics. */
91
116
  export async function installPlugin(packageSpec, profileDir) {
92
117
  // `-w` is required: every profile ships a pnpm-workspace.yaml.
93
- const code = await runPnpm(['add', '-w', packageSpec], profileDir);
94
- return code === 0;
118
+ return runPnpm(['add', '-w', packageSpec], profileDir);
95
119
  }
96
120
  /** Update a package to latest, mirroring `dsh plugin add <pkg>` (pnpm installs latest). */
97
121
  export async function updatePlugin(packageName, profileDir) {
98
- const code = await runPnpm(['add', '-w', packageName], profileDir);
99
- return code === 0;
122
+ return runPnpm(['add', '-w', packageName], profileDir);
100
123
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-plugin-center",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Plugin center for DeepSeek Harness — installed metadata, community market, update detection, and What's New",
5
5
  "license": "MIT",
6
6
  "publishConfig": {