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

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
@@ -370,7 +370,7 @@ function UpdatesView({ updates, refresh, updateOne, busy }) {
370
370
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
371
371
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: busy !== null, onClick: () => {
372
372
  updateOne(u.name);
373
- }, children: busy === u.name ? "\u66F4\u65B0\u4E2D\u2026" : "\u66F4\u65B0" })
373
+ }, children: busy === u.name || busy === "__all__" ? "\u66F4\u65B0\u4E2D\u2026" : "\u66F4\u65B0" })
374
374
  ] }),
375
375
  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
376
  ] }, u.name)) });
@@ -389,12 +389,21 @@ function CenterPanel({ variant = "section" }) {
389
389
  const [marketCount, setMarketCount] = (0, import_react.useState)(0);
390
390
  const [updates, setUpdates] = (0, import_react.useState)(null);
391
391
  const [busyUpdate, setBusyUpdate] = (0, import_react.useState)(null);
392
- const refreshUpdates = (0, import_react.useCallback)(() => {
392
+ const [checking, setChecking] = (0, import_react.useState)(false);
393
+ const refreshUpdates = (0, import_react.useCallback)((silent = false) => {
394
+ setChecking(true);
393
395
  void rpc("checkUpdates", { since: new Date(Date.now() - 30 * 864e5).toISOString() }).then(
394
396
  (v) => {
395
- setUpdates(v);
397
+ const digests = v;
398
+ setUpdates(digests);
399
+ setChecking(false);
400
+ if (!silent) {
401
+ showToast(digests.length > 0 ? `\u53D1\u73B0 ${digests.length} \u4E2A\u53EF\u66F4\u65B0\u63D2\u4EF6` : "\u6240\u6709\u63D2\u4EF6\u5747\u4E3A\u6700\u65B0");
402
+ }
396
403
  },
397
404
  () => {
405
+ setChecking(false);
406
+ if (!silent) showToast("\u68C0\u67E5\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", "error");
398
407
  }
399
408
  );
400
409
  }, []);
@@ -408,8 +417,33 @@ function CenterPanel({ variant = "section" }) {
408
417
  () => {
409
418
  }
410
419
  );
411
- refreshUpdates();
420
+ refreshUpdates(true);
412
421
  }, [refreshUpdates]);
422
+ (0, import_react.useEffect)(() => {
423
+ let alive = true;
424
+ let timer = null;
425
+ const poll = async () => {
426
+ if (!alive) return;
427
+ try {
428
+ const r = await rpc("listMarket", { source: "all" });
429
+ if (!alive) return;
430
+ marketCache.all = { plugins: r.plugins, done: r.done };
431
+ setMarketCount(r.plugins.length);
432
+ if (!r.done) timer = setTimeout(() => {
433
+ void poll();
434
+ }, 5e3);
435
+ } catch {
436
+ if (alive) timer = setTimeout(() => {
437
+ void poll();
438
+ }, 15e3);
439
+ }
440
+ };
441
+ void poll();
442
+ return () => {
443
+ alive = false;
444
+ if (timer !== null) clearTimeout(timer);
445
+ };
446
+ }, []);
413
447
  const updateOne = (name) => {
414
448
  setBusyUpdate(name);
415
449
  void rpc("update", { name }).then(
@@ -423,12 +457,26 @@ function CenterPanel({ variant = "section" }) {
423
457
  }
424
458
  );
425
459
  };
426
- const updateAll = () => {
427
- if (updates !== null) {
428
- for (const u of updates) {
429
- void updateOne(u.name);
460
+ const updateAll = async () => {
461
+ if (updates === null || updates.length === 0) return;
462
+ setBusyUpdate("__all__");
463
+ let okCount = 0;
464
+ const failures = [];
465
+ for (const u of updates) {
466
+ try {
467
+ await rpc("update", { name: u.name });
468
+ okCount++;
469
+ } catch (e) {
470
+ failures.push(`${u.name}\uFF1A${e instanceof Error ? e.message : String(e)}`);
430
471
  }
431
472
  }
473
+ setBusyUpdate(null);
474
+ if (failures.length === 0) {
475
+ showToast(`\u5DF2\u66F4\u65B0 ${okCount} \u4E2A\u63D2\u4EF6\uFF0C\u91CD\u542F dsh web \u540E\u751F\u6548`);
476
+ } else {
477
+ showToast(`\u66F4\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${okCount}\uFF0C\u5931\u8D25 ${failures.length}\uFF08${failures.join("\uFF1B")}\uFF09`, "error");
478
+ }
479
+ refreshUpdates();
432
480
  };
433
481
  const tab = (v, label, count) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: `pc-tab${view === v ? " active" : ""}`, onClick: () => {
434
482
  setView(v);
@@ -452,9 +500,13 @@ function CenterPanel({ variant = "section" }) {
452
500
  failedCount
453
501
  ] }),
454
502
  /* @__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",
503
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", disabled: checking, onClick: () => {
504
+ refreshUpdates();
505
+ }, children: checking ? "\u68C0\u67E5\u4E2D\u2026" : "\u68C0\u67E5\u66F4\u65B0" }),
506
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { className: "pc-btn primary", disabled: !updates?.length || busyUpdate !== null, onClick: () => {
507
+ void updateAll();
508
+ }, children: [
509
+ "\u66F4\u65B0\u5168\u90E8\uFF08",
458
510
  updates?.length ?? 0,
459
511
  "\uFF09"
460
512
  ] })
@@ -563,7 +615,28 @@ function Toast() {
563
615
  }
564
616
  function WhatsNewDialog() {
565
617
  const open = useWhatsNewOpen();
618
+ const [busy, setBusy] = (0, import_react.useState)(false);
566
619
  if (!open || whatsNewDigests.length === 0) return null;
620
+ const updateNow = async () => {
621
+ setBusy(true);
622
+ let okCount = 0;
623
+ const failures = [];
624
+ for (const u of whatsNewDigests) {
625
+ try {
626
+ await rpc("update", { name: u.name });
627
+ okCount++;
628
+ } catch (e) {
629
+ failures.push(`${u.name}\uFF1A${e instanceof Error ? e.message : String(e)}`);
630
+ }
631
+ }
632
+ setBusy(false);
633
+ if (failures.length === 0) {
634
+ showToast(`\u5DF2\u66F4\u65B0 ${okCount} \u4E2A\u63D2\u4EF6\uFF0C\u91CD\u542F dsh web \u540E\u751F\u6548`);
635
+ closeWhatsNew();
636
+ } else {
637
+ showToast(`\u66F4\u65B0\u5B8C\u6210\uFF1A\u6210\u529F ${okCount}\uFF0C\u5931\u8D25 ${failures.length}\uFF08${failures.join("\uFF1B")}\uFF09`, "error");
638
+ }
639
+ };
567
640
  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: [
568
641
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-panel-head", children: [
569
642
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-title", children: "\u63D2\u4EF6\u66F4\u65B0" }),
@@ -586,7 +659,10 @@ function WhatsNewDialog() {
586
659
  ] }, u.name)),
587
660
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", justifyContent: "flex-end", gap: "8px", marginTop: "16px" }, children: [
588
661
  /* @__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" })
662
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn", onClick: closeWhatsNew, children: "\u5168\u90E8\u6807\u8BB0\u5DF2\u8BFB" }),
663
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: busy, onClick: () => {
664
+ void updateNow();
665
+ }, children: busy ? "\u66F4\u65B0\u4E2D\u2026" : "\u7ACB\u5373\u66F4\u65B0" })
590
666
  ] })
591
667
  ] })
592
668
  ] }) });
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;
@@ -65,6 +67,8 @@ export declare class PluginCenterEngine extends Service {
65
67
  checkUpdates(sinceIso: string): Promise<UpdateDigest[]>;
66
68
  install(spec: string): Promise<boolean>;
67
69
  update(name: string): Promise<boolean>;
70
+ /** 串行执行一次 pnpm 操作并失效缓存(无论成败都放行链条后续任务)。 */
71
+ private enqueuePnpm;
68
72
  /** Temporary diagnostics for the empty-update bug; removed once root-caused. */
69
73
  debug(): Promise<{
70
74
  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 ok = await op();
235
+ this.installedNamesCache = null;
236
+ this.updatesCache = null;
237
+ return ok;
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/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.1",
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": {