@max-null/dsh-plugin-center 0.2.0 → 0.2.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/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,64 +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 plugin = marketCache.all?.plugins.find((p) => p.spec === item.name || p.name === item.name);
1129
- const installed = plugin?.installed === true || (installedCache ?? []).some((p) => p.name === item.name);
1130
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-card", children: [
1131
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-row", children: [
1132
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-name", children: item.name }),
1133
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
1134
- plugin !== void 0 && plugin.stars !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "pc-ver", children: [
1135
- "\u2605 ",
1136
- plugin.stars
1137
- ] }),
1138
- installed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: t("installedTag") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1139
- "button",
1140
- {
1141
- className: "pc-btn primary",
1142
- onClick: () => {
1143
- if (plugin === void 0) {
1144
- showToast(t("installFailed", { e: "\u672A\u5728\u5E02\u573A\u76EE\u5F55\u4E2D\u627E\u5230\u8BE5\u63D2\u4EF6\uFF0C\u8BF7\u624B\u52A8\u5B89\u88C5" }), "error", 8e3);
1145
- return;
1146
- }
1147
- void rpc("install", { spec: plugin.spec }).then(
1148
- () => {
1149
- pendingInstall.add(plugin.spec);
1150
- showToast(t("installQueued", { n: item.name }), "ok", 5e3);
1151
- },
1152
- (e) => showToast(t("installFailed", { e: e instanceof Error ? e.message : String(e) }), "error", 15e3)
1153
- );
1154
- },
1155
- children: t("install")
1156
- }
1157
- )
1158
- ] }),
1159
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-desc", children: item.reason })
1160
- ] }, item.name);
1161
- }) })
1162
- ] });
1163
1081
  const marketToolbar = view === "market" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-filter", children: [
1164
- aiBar,
1165
1082
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-toolbar pc-toolbar-main", children: [
1166
1083
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "pc-search", style: { flex: 1, minWidth: 120 }, value: marketSearch, onChange: (e) => {
1167
1084
  setMarketSearch(e.target.value);
package/dist/engine.d.ts CHANGED
@@ -26,6 +26,10 @@ export interface MarketSnapshot {
26
26
  export interface Suggestion {
27
27
  name: string;
28
28
  reason: string;
29
+ /** Install spec resolved at suggest time (npm name or github:owner/repo). */
30
+ spec: string | null;
31
+ /** GitHub stars when the catalog knows them. */
32
+ stars: number | null;
29
33
  }
30
34
  /** One-shot diagnostics report (diagnostics). */
31
35
  export interface DiagnosticsReport {
@@ -112,6 +116,8 @@ export declare class PluginCenterEngine extends Service {
112
116
  screenshot(name: string): Promise<string | null>;
113
117
  /** AI recommendation: keyword-filtered candidates ranked by the model. */
114
118
  suggest(query: string): Promise<Suggestion[]>;
115
- /** Wait for the dsh-market catalog (fetch or failure), with a hard deadline. */
116
- private waitDshMarket;
119
+ /** Wait for all three market sources (fetch or failure), with a hard deadline. */
120
+ private waitAllSources;
121
+ /** 三源市场合并(按 name 去重;spec 优先 npm 名——比 github 回退更可靠)。 */
122
+ private combinedMarketCache;
117
123
  }
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 = {
@@ -286,7 +287,25 @@ export class PluginCenterEngine extends Service {
286
287
  return this.updatesCache.digests;
287
288
  }
288
289
  async install(spec) {
289
- return this.enqueuePnpm(() => installPlugin(spec, this.baseUrl));
290
+ return this.enqueuePnpm(async () => {
291
+ // Snapshot inside the serialized chain: concurrent installs would
292
+ // otherwise diff each other's additions.
293
+ const before = readDependencyKeys(this.baseUrl);
294
+ const result = await installPlugin(spec, this.baseUrl);
295
+ if (result.ok) {
296
+ // `dsh plugin add` reconciles bundles into dsh.profile.bundles and
297
+ // reports non-plugin repos; mirror that so "restart to take effect"
298
+ // is true for real bundles and honest for repo-only packages
299
+ // (2026-08-22: EAC 装完不生效——无插件入口,提示误导).
300
+ try {
301
+ const { note } = reconcileInstalled(this.baseUrl, before);
302
+ if (note !== '')
303
+ result.detail = note;
304
+ }
305
+ catch { /* best-effort */ }
306
+ }
307
+ return result;
308
+ });
290
309
  }
291
310
  /** Update one installed plugin to the detected target version (exact — see update.ts). */
292
311
  async update(name, version) {
@@ -356,13 +375,15 @@ export class PluginCenterEngine extends Service {
356
375
  const q = query.trim();
357
376
  if (q === '')
358
377
  return [];
359
- await this.waitDshMarket();
378
+ await this.waitAllSources();
360
379
  const llm = this.ctx.get('llm');
361
380
  if (llm?.stream === undefined) {
362
381
  throw new Error('llm 服务不可用(当前 profile 未提供 dsh-llm)');
363
382
  }
364
383
  const tokens = q.toLowerCase().split(/[\s,,、;;]+/u).filter(Boolean);
365
- const scored = this.dshMarketCache
384
+ // 候选 = 三源合并(2026-08-22:此前只用 dsh-market,awesome/Oh-My-DSH
385
+ // 的精选插件进不了推荐;合并后按 name 去重、spec 优先 npm 名)。
386
+ const scored = this.combinedMarketCache()
366
387
  .map(p => ({
367
388
  p,
368
389
  hits: tokens.reduce((n, tok) => n + (p.name.toLowerCase().includes(tok)
@@ -375,25 +396,40 @@ export class PluginCenterEngine extends Service {
375
396
  if (scored.length === 0)
376
397
  return [];
377
398
  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":"一句话中文推荐理由"}]';
399
+ const system = '你是 DeepSeek Harness 插件市场的推荐助手。根据用户需求从候选插件中选择 3-5 个最合适的,只输出一个 JSON 数组(不要 markdown 代码块、不要任何多余文字):[{"name":"插件名","reason":"一句话中文推荐理由(20 字以内)"}]。名称必须从候选列表原样复制,禁止改写、拼接或编造';
400
+ // Message 契约:content 是 ContentBlock 数组(非字符串),且需要 id/source。
379
401
  const chunks = llm.stream({
380
402
  provider: 'deepseek-official',
381
403
  model: 'deepseek-v4-flash',
382
- messages: [{ role: 'user', content: `用户需求:${q}\n\n候选插件列表(名称 | 星标 | 简介):\n${list}` }],
404
+ messages: [{
405
+ id: 'plugin-center-suggest',
406
+ role: 'user',
407
+ content: [{ type: 'text', text: `用户需求:${q}\n\n候选插件列表(名称 | 星标 | 简介):\n${list}` }],
408
+ source: { kind: 'plugin', plugin: 'dsh-plugin-center' },
409
+ }],
383
410
  system,
384
- maxTokens: 800,
385
- signal: AbortSignal.timeout(40000),
411
+ // 800 会把 3-5 条中文推荐截断成不完整 JSON(2026-08-22 实测
412
+ // "模型输出无法解析" + 截断的 JSON 片段);2000 留足余量。
413
+ maxTokens: 2000,
414
+ signal: AbortSignal.timeout(90000),
386
415
  });
387
416
  let text = '';
388
417
  let failed = false;
418
+ let failureDetail = '';
389
419
  for await (const chunk of chunks) {
390
420
  if (chunk.type === 'text-delta')
391
421
  text += chunk.text ?? '';
392
- else if (chunk.type === 'finish' && (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted'))
422
+ else if (chunk.type === 'finish' && (chunk.reason?.kind === 'error' || chunk.reason?.kind === 'aborted')) {
393
423
  failed = true;
424
+ // LlmFailure { code, message }——必须透出,否则用户只看到笼统的
425
+ // "模型推荐失败"而无法诊断(2026-08-22 实测空 text + 无详情)。
426
+ const f = chunk.reason.failure;
427
+ failureDetail = f ? `${f.code ?? 'unknown'}: ${f.message ?? ''}` : chunk.reason.kind;
428
+ }
429
+ }
430
+ if (failed || text.trim() === '') {
431
+ throw new Error(`模型推荐失败,请重试${failureDetail !== '' ? `(${failureDetail.slice(0, 200)})` : text === '' ? '' : `(模型返回:${text.slice(0, 80)})`}`);
394
432
  }
395
- if (failed || text.trim() === '')
396
- throw new Error('模型推荐失败,请重试');
397
433
  const start = text.indexOf('[');
398
434
  const end = text.lastIndexOf(']');
399
435
  if (start < 0 || end <= start)
@@ -402,22 +438,70 @@ export class PluginCenterEngine extends Service {
402
438
  const parsed = JSON.parse(text.slice(start, end + 1));
403
439
  if (!Array.isArray(parsed))
404
440
  throw new Error('bad shape');
441
+ // Attach the catalog identity (spec/stars) here, so the client never
442
+ // has to reverse-match by name — the model may echo either the npm
443
+ // name or the owner/repo form (2026-08-22: stars went missing because
444
+ // "Deepseek-Harness-EAC" (npm) did not match the cache key
445
+ // "zouyuxuan122/Deepseek-Harness-EAC").
405
446
  return parsed
406
447
  .filter((item) => item !== null && typeof item === 'object'
407
448
  && typeof item.name === 'string'
408
449
  && typeof item.reason === 'string')
409
- .slice(0, 5);
450
+ .slice(0, 5)
451
+ .map(item => {
452
+ // 模型可能改写 owner/路径(幻觉,如 zhu1090093659 非真实 owner)
453
+ // 或输出 monorepo 子路径——先精确匹配(name/spec/npm),再 # 截断,
454
+ // 最后回退 repo 名匹配(2026-08-22:DamonKoy/dsh-web-ui#dsh-web-ui-all
455
+ // 被输出成 zhu1090093659/dsh-web-ui#packages/...,精确匹配失败无星)。
456
+ const base = item.name.split('#')[0].trim();
457
+ const repo = base.split('/').pop() ?? '';
458
+ const hit = this.combinedMarketCache().find(p => p.name === base || p.spec === base || p.npm === base
459
+ || (p.name.split('/').pop()?.split('#')[0] ?? '') === repo);
460
+ return {
461
+ name: item.name,
462
+ reason: item.reason,
463
+ spec: hit?.spec ?? null,
464
+ stars: hit?.stars ?? null,
465
+ };
466
+ });
410
467
  }
411
468
  catch {
412
- throw new Error(`模型输出无法解析:${text.slice(0, 200)}`);
469
+ // 截断的 JSON(maxTokens 用尽/中止)会解析失败——展示更长片段辅助诊断。
470
+ throw new Error(`模型输出无法解析:${text.slice(0, 400)}`);
413
471
  }
414
472
  }
415
- /** Wait for the dsh-market catalog (fetch or failure), with a hard deadline. */
416
- async waitDshMarket() {
473
+ /** Wait for all three market sources (fetch or failure), with a hard deadline. */
474
+ async waitAllSources() {
475
+ this.prefetchAwesome();
476
+ this.prefetchOhMyDsh();
417
477
  this.prefetchDshMarket();
418
478
  const deadline = Date.now() + 65_000;
419
- while (!this.dshMarketDone && Date.now() < deadline) {
479
+ while (!(this.awesomeDone && this.ohMyDshDone && this.dshMarketDone) && Date.now() < deadline) {
420
480
  await new Promise(resolve => setTimeout(resolve, 200));
421
481
  }
422
482
  }
483
+ /** 三源市场合并(按 name 去重;spec 优先 npm 名——比 github 回退更可靠)。 */
484
+ combinedMarketCache() {
485
+ const map = new Map();
486
+ for (const list of [this.awesomeCache, this.ohMyDshCache, this.dshMarketCache]) {
487
+ for (const p of list) {
488
+ const cur = map.get(p.name);
489
+ if (cur === undefined) {
490
+ map.set(p.name, p);
491
+ continue;
492
+ }
493
+ map.set(p.name, {
494
+ ...cur,
495
+ spec: p.npm ?? cur.npm ?? p.spec ?? cur.spec,
496
+ npm: cur.npm ?? p.npm,
497
+ // 同一条目多源星数不同:取最大值(2026-08-22:awesome ★4 曾
498
+ // 覆盖 dsh-market 高星——合并顺序导致低星胜出)。
499
+ stars: Math.max(cur.stars ?? 0, p.stars ?? 0) || null,
500
+ score: cur.score ?? p.score,
501
+ description: cur.description.zh !== '' && cur.description.en !== '' ? cur.description : p.description,
502
+ });
503
+ }
504
+ }
505
+ return [...map.values()];
506
+ }
423
507
  }
package/dist/market.js CHANGED
@@ -59,6 +59,22 @@ export async function fetchOhMyDshOverrides() {
59
59
  return {};
60
60
  }
61
61
  }
62
+ /**
63
+ * Derive an install spec for a dsh-market entry: a reliable single command
64
+ * parsed from the README (`dsh plugin add <spec>` / `pnpm add <spec>` /
65
+ * `git clone`) yields its package/spec; anything else falls back to the
66
+ * GitHub form (`github:owner/repo`), which dsh always accepts — many
67
+ * catalog entries are NOT on npm (e.g. Deepseek-Harness-EAC 404, 2026-08-22).
68
+ */
69
+ function installSpecOf(p, name, full) {
70
+ const cmd = (p.install?.commands ?? []).find(c => /\b(?:dsh\s+plugin\s+add|pnpm\s+add|npm\s+install|git\s+clone)\b/u.test(c));
71
+ if (cmd !== undefined) {
72
+ const match = /\b(?:add|install)\s+([^\s"'`]+)/u.exec(cmd);
73
+ if (match !== null && !/<[^>]+>/u.test(match[1]))
74
+ return match[1];
75
+ }
76
+ return `github:${full}`;
77
+ }
62
78
  /** Category keywords for dsh-market tag → CATEGORIES mapping (prefix match). */
63
79
  const CATEGORY_KEYWORDS = [
64
80
  ['ui', ['ui', 'interface', 'sidebar', 'panel', 'widget', '界面', '面板', '侧栏', '导航']],
@@ -101,6 +117,17 @@ export async function fetchDshMarketPlugins() {
101
117
  const json = await res.json();
102
118
  const out = [];
103
119
  for (const p of json.plugins ?? []) {
120
+ // Skill entries install into ~/.agents/skills via a different mechanism
121
+ // (npx skills add); the pnpm-driven center cannot install them, so they
122
+ // are excluded from the catalog rather than offered with a broken spec.
123
+ if (p.type === 'skill' || p.install?.method === 'skills-add')
124
+ continue;
125
+ // Entries without README install commands are usually desktop apps or
126
+ // config repos, not installable plugins (2026-08-22: 4299 条里 943 条
127
+ // 无 commands——如 Deepseek-Harness-EAC「一键启动桌面端」被当插件
128
+ // 推荐,装完不可用;installSpecOf 只能 fallback 一个假 spec)。
129
+ if (!Array.isArray(p.install?.commands) || p.install.commands.length === 0)
130
+ continue;
104
131
  const name = typeof p.name === 'string' && p.name !== '' ? p.name : '';
105
132
  const full = typeof p.fullName === 'string' && p.fullName !== '' ? p.fullName : name;
106
133
  if (full === '')
@@ -111,7 +138,7 @@ export async function fetchDshMarketPlugins() {
111
138
  // Merge key / display name: owner/repo (matches the awesome source).
112
139
  name: full,
113
140
  url: `https://github.com/${full}`,
114
- spec: name, // npm package name is the install spec
141
+ spec: installSpecOf(p, name, full),
115
142
  categories: categorizeTags(p.tags),
116
143
  description: { en: descEn, zh: descZh },
117
144
  stars: typeof p.stars === 'number' ? p.stars : null,
@@ -146,10 +173,15 @@ export async function fetchOhMyDshPlugins() {
146
173
  if (link === null)
147
174
  continue;
148
175
  const stars = Number(cells[5]);
176
+ // spec 必须用 URL 的 owner/repo——链接文本常只是包名(如
177
+ // [open-design](https://github.com/nexu-io/open-design)),
178
+ // github:open-design 是无效 spec(2026-08-22 实测:Oh-My-DSH
179
+ // 全部条目 spec 错,open-design 装不上)。
180
+ const repo = /github\.com\/([^/]+\/[^/]+)/u.exec(link[2]);
149
181
  plugins.push({
150
182
  name: link[1],
151
183
  url: link[2],
152
- spec: `github:${link[1]}`,
184
+ spec: `github:${repo?.[1] ?? link[1]}`,
153
185
  categories: category !== '' ? [category] : [],
154
186
  description: { en: '', zh: cells[6] },
155
187
  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,85 +1,85 @@
1
- {
2
- "name": "@max-null/dsh-plugin-center",
3
- "version": "0.2.0",
4
- "description": "Plugin center for DeepSeek Harness 鈥?installed metadata, community market, update detection, and What's New",
5
- "license": "MIT",
6
- "publishConfig": {
7
- "access": "public"
8
- },
9
- "type": "module",
10
- "main": "dist/index.js",
11
- "types": "dist/index.d.ts",
12
- "exports": {
13
- ".": {
14
- "types": "./dist/index.d.ts",
15
- "default": "./dist/index.js"
16
- },
17
- "./client": "./client.js",
18
- "./cordis.patch.yml": "./cordis.patch.yml",
19
- "./package.json": "./package.json"
20
- },
21
- "files": [
22
- "dist",
23
- "client.js",
24
- "cordis.patch.yml",
25
- "assets"
26
- ],
27
- "dsh": {
28
- "bundle": {
29
- "patch": "./cordis.patch.yml"
30
- },
31
- "client": {
32
- "inject": [
33
- "@deepseek-ai/dsh-client-connection",
34
- "@deepseek-ai/dsh-client-runtime",
35
- "@deepseek-ai/dsh-client-locale",
36
- "@deepseek-ai/dsh-client-ui-settings",
37
- "@deepseek-ai/dsh-client-ui-conversation",
38
- "@deepseek-ai/dsh-client-ui-slots",
39
- "@deepseek-ai/dsh-api-remotes"
40
- ],
41
- "platform": "web",
42
- "immediately": true
43
- }
44
- },
45
- "keywords": [
46
- "deepseek",
47
- "harness",
48
- "dsh",
49
- "dsh-plugin",
50
- "deepseek-harness",
51
- "plugin",
52
- "market",
53
- "update",
54
- "cordis"
55
- ],
56
- "repository": {
57
- "type": "git",
58
- "url": "git+https://github.com/Max-Null/dsh-plugin-center.git"
59
- },
60
- "scripts": {
61
- "build": "tsc -p tsconfig.json && node build-client.mjs",
62
- "typecheck": "tsc --noEmit -p tsconfig.json"
63
- },
64
- "dependencies": {
65
- "js-yaml": "^4.1.0"
66
- },
67
- "peerDependencies": {
68
- "@deepseek-ai/cordis": "^4.0.1",
69
- "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
70
- "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
71
- "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.1"
72
- },
73
- "devDependencies": {
74
- "@deepseek-ai/cordis": "^4.0.1",
75
- "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
76
- "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
77
- "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.1",
78
- "@types/js-yaml": "^4.0.9",
79
- "@types/node": "^26.2.0",
80
- "@types/react": "~18.3.1",
81
- "esbuild": "^0.24.0",
82
- "react": "^18.2.0",
83
- "typescript": "^5.5.0"
84
- }
85
- }
1
+ {
2
+ "name": "@max-null/dsh-plugin-center",
3
+ "version": "0.2.2",
4
+ "description": "Plugin center for DeepSeek Harness 鈥?installed metadata, community market, update detection, and What\u0027s New",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./client": "./client.js",
18
+ "./cordis.patch.yml": "./cordis.patch.yml",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "client.js",
24
+ "cordis.patch.yml",
25
+ "assets"
26
+ ],
27
+ "dsh": {
28
+ "bundle": {
29
+ "patch": "./cordis.patch.yml"
30
+ },
31
+ "client": {
32
+ "inject": [
33
+ "@deepseek-ai/dsh-client-connection",
34
+ "@deepseek-ai/dsh-client-runtime",
35
+ "@deepseek-ai/dsh-client-locale",
36
+ "@deepseek-ai/dsh-client-ui-settings",
37
+ "@deepseek-ai/dsh-client-ui-conversation",
38
+ "@deepseek-ai/dsh-client-ui-slots",
39
+ "@deepseek-ai/dsh-api-remotes"
40
+ ],
41
+ "platform": "web",
42
+ "immediately": true
43
+ }
44
+ },
45
+ "keywords": [
46
+ "deepseek",
47
+ "harness",
48
+ "dsh",
49
+ "dsh-plugin",
50
+ "deepseek-harness",
51
+ "plugin",
52
+ "market",
53
+ "update",
54
+ "cordis"
55
+ ],
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/Max-Null/dsh-plugin-center.git"
59
+ },
60
+ "scripts": {
61
+ "build": "tsc -p tsconfig.json \u0026\u0026 node build-client.mjs",
62
+ "typecheck": "tsc --noEmit -p tsconfig.json"
63
+ },
64
+ "dependencies": {
65
+ "js-yaml": "^4.1.0"
66
+ },
67
+ "peerDependencies": {
68
+ "@deepseek-ai/cordis": "^4.0.1",
69
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
70
+ "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
71
+ "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.1"
72
+ },
73
+ "devDependencies": {
74
+ "@deepseek-ai/cordis": "^4.0.1",
75
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
76
+ "@deepseek-ai/dsh-client-connection": "^0.1.1-rc.1",
77
+ "@deepseek-ai/dsh-host-apiproxy": "^0.1.1-rc.1",
78
+ "@types/js-yaml": "^4.0.9",
79
+ "@types/node": "^26.2.0",
80
+ "@types/react": "~18.3.1",
81
+ "esbuild": "^0.24.0",
82
+ "react": "^18.2.0",
83
+ "typescript": "^5.5.0"
84
+ }
85
+ }