@max-null/dsh-plugin-center 0.2.1 → 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 +7 -89
- package/dist/engine.d.ts +4 -2
- package/dist/engine.js +62 -8
- package/dist/market.js +12 -1
- package/dist/reconcile.d.ts +12 -0
- package/dist/reconcile.js +81 -0
- package/dist/rpc.js +1 -1
- package/dist/toggle.d.ts +1 -1
- package/dist/toggle.js +15 -1
- package/package.json +3 -3
package/client.js
CHANGED
|
@@ -413,11 +413,6 @@ var STRINGS = {
|
|
|
413
413
|
diagInstalled: "\u5DF2\u5B89\u88C5\u63D2\u4EF6\uFF08{n}\uFF09",
|
|
414
414
|
diagDisabled: "\u7981\u7528\u72B6\u6001",
|
|
415
415
|
diagPnpmLog: "pnpm \u65E5\u5FD7\uFF08\u5C3E\u90E8\uFF09",
|
|
416
|
-
aiTitle: "AI \u63A8\u8350",
|
|
417
|
-
aiPlaceholder: "\u63CF\u8FF0\u4F60\u7684\u9700\u6C42\uFF0C\u8BA9 AI \u63A8\u8350\u63D2\u4EF6\u2026\uFF08\u4F8B\u5982\uFF1A\u80FD\u9884\u89C8 Markdown \u7684\u63D2\u4EF6\uFF09",
|
|
418
|
-
aiAsk: "\u63A8\u8350",
|
|
419
|
-
aiAsking: "AI \u601D\u8003\u4E2D\u2026",
|
|
420
|
-
aiFail: "AI \u63A8\u8350\u5931\u8D25\uFF1A{e}",
|
|
421
416
|
scoreLabel: "\u8BC4\u5206",
|
|
422
417
|
screenshot: "\u622A\u56FE",
|
|
423
418
|
noScreenshot: "\u65E0\u622A\u56FE",
|
|
@@ -492,11 +487,6 @@ var STRINGS = {
|
|
|
492
487
|
diagInstalled: "Installed plugins ({n})",
|
|
493
488
|
diagDisabled: "Disabled state",
|
|
494
489
|
diagPnpmLog: "pnpm log (tail)",
|
|
495
|
-
aiTitle: "AI recommendation",
|
|
496
|
-
aiPlaceholder: "Describe what you need \u2014 e.g. a Markdown preview plugin\u2026",
|
|
497
|
-
aiAsk: "Recommend",
|
|
498
|
-
aiAsking: "AI is thinking\u2026",
|
|
499
|
-
aiFail: "AI recommendation failed: {e}",
|
|
500
490
|
scoreLabel: "Score",
|
|
501
491
|
screenshot: "Screenshot",
|
|
502
492
|
noScreenshot: "No screenshot",
|
|
@@ -667,6 +657,7 @@ function MarketView({ category, single, source, search, onCount }) {
|
|
|
667
657
|
void rpc("install", { spec: m.spec }).then(
|
|
668
658
|
(v) => {
|
|
669
659
|
const durationMs = typeof v === "object" && v !== null ? v.durationMs : void 0;
|
|
660
|
+
const detail = typeof v === "object" && v !== null ? v.detail : void 0;
|
|
670
661
|
if (v !== true && durationMs === void 0) throw new Error(t("installNotApplied"));
|
|
671
662
|
setBusy(null);
|
|
672
663
|
pendingInstall.add(m.spec);
|
|
@@ -675,7 +666,7 @@ function MarketView({ category, single, source, search, onCount }) {
|
|
|
675
666
|
if (c !== void 0) c.plugins = c.plugins.map((p) => p.name === m.name ? { ...p, installed: true } : p);
|
|
676
667
|
}
|
|
677
668
|
setItems((prev) => prev.map((p) => p.name === m.name ? { ...p, installed: true } : p));
|
|
678
|
-
showToast(t("installQueued", { n: m.name }) + (durationMs !== void 0 ? `\uFF08${(durationMs / 1e3).toFixed(1)}s\uFF09` : ""), "ok",
|
|
669
|
+
showToast(detail ?? t("installQueued", { n: m.name }) + (durationMs !== void 0 ? `\uFF08${(durationMs / 1e3).toFixed(1)}s\uFF09` : ""), "ok", 7e3);
|
|
679
670
|
},
|
|
680
671
|
(e) => {
|
|
681
672
|
setBusy(null);
|
|
@@ -728,7 +719,11 @@ function MarketView({ category, single, source, search, onCount }) {
|
|
|
728
719
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
|
|
729
720
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "pc-iconbtn", title: t("screenshot"), "aria-label": t("screenshot"), disabled: shotBusy !== null, onClick: () => {
|
|
730
721
|
showScreenshot(m);
|
|
731
|
-
}, children: shotBusy === m.name ? "\u2026" : "
|
|
722
|
+
}, children: shotBusy === m.name ? "\u2026" : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.5", "aria-hidden": "true", children: [
|
|
723
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2", y: "3.5", width: "12", height: "9", rx: "1.5" }),
|
|
724
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "8", cy: "8", r: "2.6" }),
|
|
725
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M5.5 3.5 6.4 1.8h3.2l.9 1.7" })
|
|
726
|
+
] }) }),
|
|
732
727
|
m.installed || pendingInstall.has(m.spec) ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: pendingInstall.has(m.spec) ? t("pendingRestart") : t("installedTag") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: busy !== null, onClick: () => {
|
|
733
728
|
install(m);
|
|
734
729
|
}, children: busy === m.name ? t("installing") : t("install") })
|
|
@@ -892,10 +887,6 @@ function CenterPanel({ variant = "section" }) {
|
|
|
892
887
|
const [busyUpdate, setBusyUpdate] = (0, import_react.useState)(null);
|
|
893
888
|
const [checking, setChecking] = (0, import_react.useState)(false);
|
|
894
889
|
const [togglingId, setTogglingId] = (0, import_react.useState)(null);
|
|
895
|
-
const [aiQuery, setAiQuery] = (0, import_react.useState)("");
|
|
896
|
-
const [aiBusy, setAiBusy] = (0, import_react.useState)(false);
|
|
897
|
-
const [aiResult, setAiResult] = (0, import_react.useState)(null);
|
|
898
|
-
const [aiError, setAiError] = (0, import_react.useState)(null);
|
|
899
890
|
const handleToggle = (p) => {
|
|
900
891
|
if (togglingId !== null) return;
|
|
901
892
|
const id = p.entryId;
|
|
@@ -929,23 +920,6 @@ function CenterPanel({ variant = "section" }) {
|
|
|
929
920
|
}
|
|
930
921
|
);
|
|
931
922
|
};
|
|
932
|
-
const askAi = () => {
|
|
933
|
-
const q = aiQuery.trim();
|
|
934
|
-
if (q === "" || aiBusy) return;
|
|
935
|
-
setAiBusy(true);
|
|
936
|
-
setAiResult(null);
|
|
937
|
-
setAiError(null);
|
|
938
|
-
void rpc("suggest", { query: q }).then(
|
|
939
|
-
(v) => {
|
|
940
|
-
setAiBusy(false);
|
|
941
|
-
setAiResult(v);
|
|
942
|
-
},
|
|
943
|
-
(e) => {
|
|
944
|
-
setAiBusy(false);
|
|
945
|
-
setAiError(e instanceof Error ? e.message : String(e));
|
|
946
|
-
}
|
|
947
|
-
);
|
|
948
|
-
};
|
|
949
923
|
const refreshUpdates = (0, import_react.useCallback)((silent = false) => {
|
|
950
924
|
setChecking(true);
|
|
951
925
|
void rpc("checkUpdates", { since: new Date(Date.now() - 30 * 864e5).toISOString() }).then(
|
|
@@ -1104,63 +1078,7 @@ function CenterPanel({ variant = "section" }) {
|
|
|
1104
1078
|
}, children: c }, c))
|
|
1105
1079
|
] })
|
|
1106
1080
|
] }) : null;
|
|
1107
|
-
const aiBar = /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6, flex: "none" }, children: [
|
|
1108
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-ai-row", children: [
|
|
1109
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1110
|
-
"input",
|
|
1111
|
-
{
|
|
1112
|
-
className: "pc-search",
|
|
1113
|
-
style: { flex: 1 },
|
|
1114
|
-
value: aiQuery,
|
|
1115
|
-
onChange: (e) => {
|
|
1116
|
-
setAiQuery(e.target.value);
|
|
1117
|
-
},
|
|
1118
|
-
onKeyDown: (e) => {
|
|
1119
|
-
if (e.key === "Enter") askAi();
|
|
1120
|
-
},
|
|
1121
|
-
placeholder: t("aiPlaceholder")
|
|
1122
|
-
}
|
|
1123
|
-
),
|
|
1124
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "pc-btn primary", disabled: aiBusy || aiQuery.trim() === "", onClick: askAi, children: aiBusy ? t("aiAsking") : t("aiAsk") })
|
|
1125
|
-
] }),
|
|
1126
|
-
aiError !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "pc-sub", style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("aiFail", { e: aiError }) }),
|
|
1127
|
-
aiResult !== null && aiResult.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-grid single", children: aiResult.map((item) => {
|
|
1128
|
-
const installed = (installedCache ?? []).some((p) => p.name === item.name || p.spec === item.spec || p.name === item.spec);
|
|
1129
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-card", children: [
|
|
1130
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-row", children: [
|
|
1131
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-name", children: item.name }),
|
|
1132
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-spacer" }),
|
|
1133
|
-
item.stars != null && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "pc-ver", children: [
|
|
1134
|
-
"\u2605 ",
|
|
1135
|
-
item.stars
|
|
1136
|
-
] }),
|
|
1137
|
-
installed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "pc-tag", children: t("installedTag") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1138
|
-
"button",
|
|
1139
|
-
{
|
|
1140
|
-
className: "pc-btn primary",
|
|
1141
|
-
onClick: () => {
|
|
1142
|
-
if (item.spec === void 0 || item.spec === null || item.spec === "") {
|
|
1143
|
-
showToast(t("installFailed", { e: "\u672A\u627E\u5230\u8BE5\u63D2\u4EF6\u7684\u5B89\u88C5 spec" }), "error", 8e3);
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
void rpc("install", { spec: item.spec }).then(
|
|
1147
|
-
() => {
|
|
1148
|
-
pendingInstall.add(item.spec);
|
|
1149
|
-
showToast(t("installQueued", { n: item.name }), "ok", 5e3);
|
|
1150
|
-
},
|
|
1151
|
-
(e) => showToast(t("installFailed", { e: e instanceof Error ? e.message : String(e) }), "error", 15e3)
|
|
1152
|
-
);
|
|
1153
|
-
},
|
|
1154
|
-
children: t("install")
|
|
1155
|
-
}
|
|
1156
|
-
)
|
|
1157
|
-
] }),
|
|
1158
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pc-desc", children: item.reason })
|
|
1159
|
-
] }, item.name);
|
|
1160
|
-
}) })
|
|
1161
|
-
] });
|
|
1162
1081
|
const marketToolbar = view === "market" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-filter", children: [
|
|
1163
|
-
aiBar,
|
|
1164
1082
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pc-toolbar pc-toolbar-main", children: [
|
|
1165
1083
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "pc-search", style: { flex: 1, minWidth: 120 }, value: marketSearch, onChange: (e) => {
|
|
1166
1084
|
setMarketSearch(e.target.value);
|
package/dist/engine.d.ts
CHANGED
|
@@ -116,6 +116,8 @@ export declare class PluginCenterEngine extends Service {
|
|
|
116
116
|
screenshot(name: string): Promise<string | null>;
|
|
117
117
|
/** AI recommendation: keyword-filtered candidates ranked by the model. */
|
|
118
118
|
suggest(query: string): Promise<Suggestion[]>;
|
|
119
|
-
/** Wait for
|
|
120
|
-
private
|
|
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;
|
|
121
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(() =>
|
|
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.
|
|
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
|
-
|
|
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,7 +396,7 @@ 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":"一句话中文推荐理由(20 字以内)"}]';
|
|
399
|
+
const system = '你是 DeepSeek Harness 插件市场的推荐助手。根据用户需求从候选插件中选择 3-5 个最合适的,只输出一个 JSON 数组(不要 markdown 代码块、不要任何多余文字):[{"name":"插件名","reason":"一句话中文推荐理由(20 字以内)"}]。名称必须从候选列表原样复制,禁止改写、拼接或编造';
|
|
379
400
|
// Message 契约:content 是 ContentBlock 数组(非字符串),且需要 id/source。
|
|
380
401
|
const chunks = llm.stream({
|
|
381
402
|
provider: 'deepseek-official',
|
|
@@ -428,7 +449,14 @@ export class PluginCenterEngine extends Service {
|
|
|
428
449
|
&& typeof item.reason === 'string')
|
|
429
450
|
.slice(0, 5)
|
|
430
451
|
.map(item => {
|
|
431
|
-
|
|
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);
|
|
432
460
|
return {
|
|
433
461
|
name: item.name,
|
|
434
462
|
reason: item.reason,
|
|
@@ -442,12 +470,38 @@ export class PluginCenterEngine extends Service {
|
|
|
442
470
|
throw new Error(`模型输出无法解析:${text.slice(0, 400)}`);
|
|
443
471
|
}
|
|
444
472
|
}
|
|
445
|
-
/** Wait for
|
|
446
|
-
async
|
|
473
|
+
/** Wait for all three market sources (fetch or failure), with a hard deadline. */
|
|
474
|
+
async waitAllSources() {
|
|
475
|
+
this.prefetchAwesome();
|
|
476
|
+
this.prefetchOhMyDsh();
|
|
447
477
|
this.prefetchDshMarket();
|
|
448
478
|
const deadline = Date.now() + 65_000;
|
|
449
|
-
while (!this.dshMarketDone && Date.now() < deadline) {
|
|
479
|
+
while (!(this.awesomeDone && this.ohMyDshDone && this.dshMarketDone) && Date.now() < deadline) {
|
|
450
480
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
451
481
|
}
|
|
452
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
|
+
}
|
|
453
507
|
}
|
package/dist/market.js
CHANGED
|
@@ -122,6 +122,12 @@ export async function fetchDshMarketPlugins() {
|
|
|
122
122
|
// are excluded from the catalog rather than offered with a broken spec.
|
|
123
123
|
if (p.type === 'skill' || p.install?.method === 'skills-add')
|
|
124
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;
|
|
125
131
|
const name = typeof p.name === 'string' && p.name !== '' ? p.name : '';
|
|
126
132
|
const full = typeof p.fullName === 'string' && p.fullName !== '' ? p.fullName : name;
|
|
127
133
|
if (full === '')
|
|
@@ -167,10 +173,15 @@ export async function fetchOhMyDshPlugins() {
|
|
|
167
173
|
if (link === null)
|
|
168
174
|
continue;
|
|
169
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]);
|
|
170
181
|
plugins.push({
|
|
171
182
|
name: link[1],
|
|
172
183
|
url: link[2],
|
|
173
|
-
spec: `github:${link[1]}`,
|
|
184
|
+
spec: `github:${repo?.[1] ?? link[1]}`,
|
|
174
185
|
categories: category !== '' ? [category] : [],
|
|
175
186
|
description: { en: '', zh: cells[6] },
|
|
176
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,
|
|
31
|
+
export declare function setDisabled(profileDir: string, entryId: string, disabled: boolean): Promise<ToggleResult>;
|
package/dist/toggle.js
CHANGED
|
@@ -33,6 +33,17 @@ const PROTECTED_PATTERNS = [
|
|
|
33
33
|
function isProtected(id) {
|
|
34
34
|
return PROTECTED_PATTERNS.some(pattern => pattern.test(id));
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Patch rows address a loader row by its ORIGINAL patch id, but the Loader
|
|
38
|
+
* expands `insert` children into runtime ids like `include:<id>`
|
|
39
|
+
* (2026-08-22 chinese-thinking 实证:写入 `include:chinese-thinking` 行
|
|
40
|
+
* 组合后不匹配任何行 → 禁用不生效;写入 `chinese-thinking` 即生效)。
|
|
41
|
+
* Strip the prefix so every entry we toggle writes a row the composition
|
|
42
|
+
* actually matches; non-insert rows are unaffected.
|
|
43
|
+
*/
|
|
44
|
+
function patchRowId(entryId) {
|
|
45
|
+
return entryId.replace(/^include:/u, '');
|
|
46
|
+
}
|
|
36
47
|
/** Official bundle entry ids (timer/llm/session/… from dsh-base and
|
|
37
48
|
* dsh-web-app patch inserts): disabling these breaks the core chain, so
|
|
38
49
|
* they refuse to toggle alongside the pattern-based protection above. */
|
|
@@ -89,8 +100,11 @@ let toggleChain = Promise.resolve();
|
|
|
89
100
|
* @param disabled - the target stance.
|
|
90
101
|
* @returns the outcome; `nowDisabled` mirrors the stance or null when refused.
|
|
91
102
|
*/
|
|
92
|
-
export function setDisabled(profileDir,
|
|
103
|
+
export function setDisabled(profileDir, entryId, disabled) {
|
|
93
104
|
const run = toggleChain.then(async () => {
|
|
105
|
+
// Loader runtime ids (include:<id>) never match patch composition —
|
|
106
|
+
// always address rows by their original patch id.
|
|
107
|
+
const id = patchRowId(entryId);
|
|
94
108
|
if (isProtected(id) || officialEntryIds(profileDir).has(id)) {
|
|
95
109
|
return { ok: false, detail: `"${id}" is host infrastructure and cannot be disabled`, nowDisabled: null };
|
|
96
110
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-plugin-center",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Plugin center for DeepSeek Harness
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Plugin center for DeepSeek Harness 鈥?installed metadata, community market, update detection, and What\u0027s New",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
@@ -82,4 +82,4 @@
|
|
|
82
82
|
"react": "^18.2.0",
|
|
83
83
|
"typescript": "^5.5.0"
|
|
84
84
|
}
|
|
85
|
-
}
|
|
85
|
+
}
|