@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 +7 -90
- package/dist/engine.d.ts +8 -2
- package/dist/engine.js +99 -15
- package/dist/market.js +34 -2
- 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 +85 -85
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,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
|
|
116
|
-
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;
|
|
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(() =>
|
|
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,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
|
|
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: [{
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
|
|
469
|
+
// 截断的 JSON(maxTokens 用尽/中止)会解析失败——展示更长片段辅助诊断。
|
|
470
|
+
throw new Error(`模型输出无法解析:${text.slice(0, 400)}`);
|
|
413
471
|
}
|
|
414
472
|
}
|
|
415
|
-
/** Wait for
|
|
416
|
-
async
|
|
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:
|
|
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,
|
|
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,85 +1,85 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|