dsh-plugin-marketplace 0.1.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/README.md +4 -0
- package/README.zh.md +44 -0
- package/client.js +105 -12
- package/index.js +157 -6
- package/package.json +51 -44
- package/vendor/dsh-settings-expose.js +75 -0
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# dsh-plugin-marketplace
|
|
2
2
|
|
|
3
|
+
[](README.zh.md)
|
|
4
|
+
|
|
5
|
+
**GitHub**: [Scorp1o117/dsh-plugin-marketplace](https://github.com/Scorp1o117/dsh-plugin-marketplace) · **npm**: [dsh-plugin-marketplace](https://www.npmjs.com/package/dsh-plugin-marketplace)
|
|
6
|
+
|
|
3
7
|
A **plugin marketplace** inside the DeepSeek Harness Web UI: browse
|
|
4
8
|
[github.com/topics/dsh-plugin](https://github.com/topics/dsh-plugin) right
|
|
5
9
|
from the settings page — no terminal needed.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# dsh-plugin-marketplace
|
|
2
|
+
|
|
3
|
+
**GitHub**: [Scorp1o117/dsh-plugin-marketplace](https://github.com/Scorp1o117/dsh-plugin-marketplace) · **npm**: [dsh-plugin-marketplace](https://www.npmjs.com/package/dsh-plugin-marketplace) · [English](README.md)
|
|
4
|
+
|
|
5
|
+
在 DeepSeek Harness **Web UI 里内置的插件市场**:直接在设置页浏览
|
|
6
|
+
[github.com/topics/dsh-plugin](https://github.com/topics/dsh-plugin),无需打开终端。
|
|
7
|
+
|
|
8
|
+
- **搜索**:按关键词搜索主题下的插件,按 Star 或更新时间**排序**
|
|
9
|
+
- **插件卡片**:名称、简介、Star 数、语言、更新时间一目了然
|
|
10
|
+
- **详情面板**:GitHub README 摘要、安装命令、仓库 / npm 链接
|
|
11
|
+
- 基于 GitHub 公开搜索 API(浏览器 CORS 直连,无需密钥;未认证限流 60 次/小时)
|
|
12
|
+
- 零客户端依赖(只用 React),无构建步骤 —— 手写 ModuleLoader bundle
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
在 `$DSH_HOME/profiles/web/cordis.patch.yml` 里:
|
|
17
|
+
|
|
18
|
+
```yaml
|
|
19
|
+
- insert:
|
|
20
|
+
- id: plugin-marketplace
|
|
21
|
+
name: 'dsh-plugin-marketplace'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
然后重启 `dsh web`(新客户端插件需要重启进程才会被扫描进浏览器清单),打开 **设置 → 插件市场**。
|
|
25
|
+
|
|
26
|
+
## 实现方式
|
|
27
|
+
|
|
28
|
+
| 层 | 文件 | 作用 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| 服务端壳 | `index.js` | 空 apply —— 让包成为 loader entry |
|
|
31
|
+
| 浏览器端 | `client.js` | 注册 `settings.section` 的 "marketplace" 分区;调用 GitHub 搜索 API;渲染卡片 + 详情 |
|
|
32
|
+
| 清单 | `package.json` | `dsh.client: { platform: "web" }` + `exports["./client"]` —— 被 `dsh-client-modules` 扫描发现 |
|
|
33
|
+
|
|
34
|
+
浏览器端不需要任何 `dsh.client.inject` 依赖包:只用 `react`(web 运行时自带)和 `slots` / `locale` 客户端服务。
|
|
35
|
+
|
|
36
|
+
## 备注
|
|
37
|
+
|
|
38
|
+
- GitHub 搜索 API 最多返回 1000 条;该主题目前有 280+ 仓库,翻页可以覆盖全部。
|
|
39
|
+
- README 按需按插件拉取,截断到约 1200 字符。
|
|
40
|
+
- 遇到 "rate-limited":等一小时,或让 web 走带 GitHub token 的代理。
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
package/client.js
CHANGED
|
@@ -19,6 +19,7 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
|
|
20
20
|
// ── CSS (theme tokens) ────────────────────────────────────────────────
|
|
21
21
|
var CSS = ".__mp_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;padding:0;margin:0;list-style:none}" +
|
|
22
|
+
".__mp_item{display:flex;flex-direction:column;gap:8px}" +
|
|
22
23
|
".__mp_card{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:6px;background:var(--dsw-alias-bg-layer-2);cursor:pointer;text-align:left;font:inherit;color:inherit}" +
|
|
23
24
|
".__mp_card:hover{border-color:var(--dsw-alias-brand-primary)}" +
|
|
24
25
|
".__mp_cardHead{display:flex;align-items:center;gap:8px;min-width:0}" +
|
|
@@ -38,6 +39,7 @@ window.__ModuleLoader__.load({
|
|
|
38
39
|
".__mp_readme{font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary);white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto;width:100%;box-sizing:border-box;min-width:0}" +
|
|
39
40
|
".__mp_code{display:block;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:6px;padding:8px 10px;font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-all;width:100%;box-sizing:border-box;min-width:0}" +
|
|
40
41
|
".__mp_link{color:var(--dsw-alias-brand-primary);font-size:12px;text-decoration:none}" +
|
|
42
|
+
".__mp_btnPrimary{border-color:var(--dsw-alias-state-business-primary, #679efe);background:var(--dsw-alias-state-business-primary, #679efe);color:#fff}" +
|
|
41
43
|
".__mp_error{color:var(--dsw-alias-label-error);font-size:12px;margin:8px 0 0}";
|
|
42
44
|
var tagId = "dsh-plugin-marketplace/main.css";
|
|
43
45
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
@@ -50,7 +52,7 @@ window.__ModuleLoader__.load({
|
|
|
50
52
|
|
|
51
53
|
// ── locale ────────────────────────────────────────────────────────────
|
|
52
54
|
var NS = "marketplace";
|
|
53
|
-
var inject = ["slots", "locale"];
|
|
55
|
+
var inject = ["slots", "locale", "settingsScope", "connection"];
|
|
54
56
|
var zh = {
|
|
55
57
|
nav: "插件市场",
|
|
56
58
|
search: "搜索插件(关键词或留空浏览全部)…",
|
|
@@ -68,7 +70,14 @@ window.__ModuleLoader__.load({
|
|
|
68
70
|
openNpm: "在 npm 搜索同名包 ↗",
|
|
69
71
|
updated: "更新",
|
|
70
72
|
total: "共 {count} 个插件",
|
|
71
|
-
ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试"
|
|
73
|
+
ghError: "GitHub API 请求失败(未认证限流 60 次/小时),请稍后再试",
|
|
74
|
+
installBtn: "一键安装",
|
|
75
|
+
confirmInstall: "确认安装 {pkg}?",
|
|
76
|
+
installing: "安装中…",
|
|
77
|
+
installOk: "已安装:{msg}",
|
|
78
|
+
installErr: "安装失败:{msg}",
|
|
79
|
+
installIdle: "",
|
|
80
|
+
installingHint: "正在后台执行 dsh plugin add,请稍候…"
|
|
72
81
|
};
|
|
73
82
|
var en = {
|
|
74
83
|
nav: "Plugin Marketplace",
|
|
@@ -87,7 +96,14 @@ window.__ModuleLoader__.load({
|
|
|
87
96
|
openNpm: "Search npm ↗",
|
|
88
97
|
updated: "Updated",
|
|
89
98
|
total: "{count} plugins",
|
|
90
|
-
ghError: "GitHub API rate-limited (60/hr unauthenticated), try again later"
|
|
99
|
+
ghError: "GitHub API rate-limited (60/hr unauthenticated), try again later",
|
|
100
|
+
installBtn: "Install",
|
|
101
|
+
confirmInstall: "Install {pkg}?",
|
|
102
|
+
installing: "Installing…",
|
|
103
|
+
installOk: "Installed: {msg}",
|
|
104
|
+
installErr: "Install failed: {msg}",
|
|
105
|
+
installIdle: "",
|
|
106
|
+
installingHint: "Running dsh plugin add in the background…"
|
|
91
107
|
};
|
|
92
108
|
|
|
93
109
|
// ── GitHub API ────────────────────────────────────────────────────────
|
|
@@ -128,7 +144,11 @@ window.__ModuleLoader__.load({
|
|
|
128
144
|
async function fetchReadme(fullName) {
|
|
129
145
|
var url = "https://api.github.com/repos/" + fullName + "/readme";
|
|
130
146
|
var res = await fetch(url, { headers: { Accept: "application/vnd.github+json" } });
|
|
131
|
-
if (!res.ok)
|
|
147
|
+
if (!res.ok) {
|
|
148
|
+
var e = new Error("readme http " + res.status);
|
|
149
|
+
e.status = res.status;
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
132
152
|
var data = await res.json();
|
|
133
153
|
var bin = atob(data.content.replace(/\s+/g, ""));
|
|
134
154
|
var bytes = new Uint8Array(bin.length);
|
|
@@ -156,6 +176,32 @@ window.__ModuleLoader__.load({
|
|
|
156
176
|
function DetailPanel(props) {
|
|
157
177
|
var p = props.plugin;
|
|
158
178
|
var installName = p.fullName.split("/")[1];
|
|
179
|
+
var st = props.installState; // {status, message, pkg} from the settings scope
|
|
180
|
+
var active = st && st.pkg === installName;
|
|
181
|
+
var status = active ? st.status : "idle";
|
|
182
|
+
var [confirming, setConfirming] = react.useState(false);
|
|
183
|
+
var statusNode = null;
|
|
184
|
+
if (status === "running") {
|
|
185
|
+
statusNode = h("div", { className: "__mp_status" }, props.t("installingHint"));
|
|
186
|
+
} else if (status === "ok") {
|
|
187
|
+
statusNode = h("div", { className: "__mp_status" }, props.t("installOk").replace("{msg}", st.message || ""));
|
|
188
|
+
} else if (status === "error") {
|
|
189
|
+
statusNode = h("div", { className: "__mp_error" }, props.t("installErr").replace("{msg}", st.message || ""));
|
|
190
|
+
}
|
|
191
|
+
var btn;
|
|
192
|
+
if (status === "running") {
|
|
193
|
+
btn = h("button", { type: "button", className: "__mp_more", disabled: true }, props.t("installing"));
|
|
194
|
+
} else if (confirming) {
|
|
195
|
+
btn = h("button", {
|
|
196
|
+
type: "button", className: "__mp_btnPrimary __mp_more",
|
|
197
|
+
onClick: function () { setConfirming(false); props.onInstall(installName); }
|
|
198
|
+
}, props.t("confirmInstall").replace("{pkg}", installName));
|
|
199
|
+
} else {
|
|
200
|
+
btn = h("button", {
|
|
201
|
+
type: "button", className: "__mp_btnPrimary __mp_more",
|
|
202
|
+
onClick: function () { setConfirming(true); }
|
|
203
|
+
}, props.t("installBtn"));
|
|
204
|
+
}
|
|
159
205
|
return h("div", { className: "__mp_detail" },
|
|
160
206
|
h("div", { className: "__mp_detailTitle" }, p.fullName),
|
|
161
207
|
h("div", { className: "__mp_meta" },
|
|
@@ -175,9 +221,12 @@ window.__ModuleLoader__.load({
|
|
|
175
221
|
"# then in $DSH_HOME/profiles/web/cordis.patch.yml:\n" +
|
|
176
222
|
"- insert:\n - id: " + installName + "\n name: '" + installName + "'"
|
|
177
223
|
),
|
|
224
|
+
btn,
|
|
225
|
+
statusNode,
|
|
178
226
|
h("div", { className: "__mp_detailTitle" }, props.t("readme")),
|
|
179
227
|
h("div", { className: "__mp_readme" },
|
|
180
228
|
props.readmeLoading ? props.t("loading")
|
|
229
|
+
: props.readmeRateLimited ? props.ghError
|
|
181
230
|
: props.readmeError ? props.t("readmeEmpty")
|
|
182
231
|
: (props.readme || "")
|
|
183
232
|
)
|
|
@@ -186,8 +235,33 @@ window.__ModuleLoader__.load({
|
|
|
186
235
|
|
|
187
236
|
function MarketplaceSection(props) {
|
|
188
237
|
var t = props.t;
|
|
189
|
-
var
|
|
238
|
+
var scope = props.scope;
|
|
239
|
+
var api = props.api;
|
|
240
|
+
var state = react.useState({ q: "", sort: "stars", page: 1, items: [], loading: false, error: null, total: 0, open: null, readme: null, readmeLoading: false, readmeError: false, readmeRateLimited: false });
|
|
190
241
|
var s = state[0], set = state[1];
|
|
242
|
+
// install-state subscription (stable pattern: useState + subscribe, never
|
|
243
|
+
// depend on getSnapshot() reference identity).
|
|
244
|
+
var [installState, setInstallState] = react.useState(null);
|
|
245
|
+
react.useEffect(function () {
|
|
246
|
+
var alive = true;
|
|
247
|
+
var sync = function () { if (alive) setInstallState(scope.getSnapshot()); };
|
|
248
|
+
sync();
|
|
249
|
+
var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
|
|
250
|
+
return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
|
|
251
|
+
}, [scope]);
|
|
252
|
+
var onInstall = react.useCallback(function (pkg) {
|
|
253
|
+
api.settings.mutate({
|
|
254
|
+
ns: "plugin-marketplace",
|
|
255
|
+
ops: [{ op: "set", path: ["install"], value: { pkg: pkg, ts: Date.now() } }]
|
|
256
|
+
}).then(function (response) {
|
|
257
|
+
if (!response.result.ok) {
|
|
258
|
+
var detail = response.result.error || {};
|
|
259
|
+
set(function (prev) { return Object.assign({}, prev, { installError: String(detail.message || detail.code || "unknown") }); });
|
|
260
|
+
}
|
|
261
|
+
}).catch(function (e) {
|
|
262
|
+
set(function (prev) { return Object.assign({}, prev, { installError: String(e && e.message || e) }); });
|
|
263
|
+
});
|
|
264
|
+
}, [api]);
|
|
191
265
|
var load = react.useCallback(function (q, sort, page, append) {
|
|
192
266
|
set(function (prev) { return Object.assign({}, prev, { loading: true, error: null }); });
|
|
193
267
|
fetchPage(q, sort, page).then(function (out) {
|
|
@@ -204,16 +278,23 @@ window.__ModuleLoader__.load({
|
|
|
204
278
|
}, [load]);
|
|
205
279
|
var openDetail = react.useCallback(function (plugin) {
|
|
206
280
|
if (s.open && s.open.fullName === plugin.fullName) { set(function (prev) { return Object.assign({}, prev, { open: null }); }); return; }
|
|
207
|
-
set(function (prev) { return Object.assign({}, prev, { open: plugin, readme: null, readmeError: false, readmeLoading: true }); });
|
|
281
|
+
set(function (prev) { return Object.assign({}, prev, { open: plugin, readme: null, readmeError: false, readmeRateLimited: false, readmeLoading: true }); });
|
|
282
|
+
// Detail expands inline under the clicked card; scroll minimally
|
|
283
|
+
// (nearest) only when it would fall outside the viewport.
|
|
284
|
+
setTimeout(function () {
|
|
285
|
+
var el = document.querySelector(".__mp_detail");
|
|
286
|
+
if (el) el.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
287
|
+
}, 50);
|
|
208
288
|
fetchReadme(plugin.fullName).then(function (text) {
|
|
209
289
|
set(function (prev) {
|
|
210
290
|
if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
|
|
211
|
-
return Object.assign({}, prev, { readme: text.slice(0, 1200), readmeLoading: false });
|
|
291
|
+
return Object.assign({}, prev, { readme: text.slice(0, 1200), readmeLoading: false, readmeError: false, readmeRateLimited: false });
|
|
212
292
|
});
|
|
213
|
-
}).catch(function () {
|
|
293
|
+
}).catch(function (err) {
|
|
214
294
|
set(function (prev) {
|
|
215
295
|
if (!prev.open || prev.open.fullName !== plugin.fullName) return prev;
|
|
216
|
-
|
|
296
|
+
var rateLimited = err && (err.status === 403 || err.status === 429);
|
|
297
|
+
return Object.assign({}, prev, { readmeLoading: false, readmeError: !rateLimited, readmeRateLimited: rateLimited });
|
|
217
298
|
});
|
|
218
299
|
});
|
|
219
300
|
}, [s.open]);
|
|
@@ -232,13 +313,21 @@ window.__ModuleLoader__.load({
|
|
|
232
313
|
),
|
|
233
314
|
s.total > 0 ? h("p", { className: "__mp_status" }, t("total").replace("{count}", String(s.total))) : null,
|
|
234
315
|
h("ul", { className: "__mp_grid" },
|
|
235
|
-
s.items.map(function (p) {
|
|
316
|
+
s.items.map(function (p) {
|
|
317
|
+
// Detail expands INLINE right under the clicked card, so there is
|
|
318
|
+
// no jumping around the page.
|
|
319
|
+
var open = s.open && s.open.fullName === p.fullName;
|
|
320
|
+
return h("div", { key: p.fullName, className: "__mp_item" },
|
|
321
|
+
h(PluginCard, { plugin: p, t: t, onOpen: function () { openDetail(p); } }),
|
|
322
|
+
open ? h(DetailPanel, { plugin: s.open, t: t, readme: s.readme, readmeLoading: s.readmeLoading, readmeError: s.readmeError, readmeRateLimited: s.readmeRateLimited, lang: s.open.lang, installState: installState && installState.status === "ready" ? installState.value.installState : null, onInstall: onInstall, ghError: t("ghError") }) : null
|
|
323
|
+
);
|
|
324
|
+
})
|
|
236
325
|
),
|
|
237
326
|
s.loading ? h("p", { className: "__mp_status" }, t("loading")) : null,
|
|
238
327
|
s.error ? h("p", { className: "__mp_error" }, s.error) : null,
|
|
239
328
|
s.items.length === 0 && !s.loading && !s.error ? h("p", { className: "__mp_status" }, t("empty")) : null,
|
|
240
329
|
s.items.length > 0 ? h("button", { type: "button", className: "__mp_more", onClick: more, disabled: s.loading }, t("loadMore")) : null,
|
|
241
|
-
s.
|
|
330
|
+
s.installError ? h("p", { className: "__mp_error" }, s.installError) : null
|
|
242
331
|
);
|
|
243
332
|
}
|
|
244
333
|
|
|
@@ -246,6 +335,8 @@ window.__ModuleLoader__.load({
|
|
|
246
335
|
function apply(ctx) {
|
|
247
336
|
var t = ctx.locale.bind(NS);
|
|
248
337
|
ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-plugin-marketplace: dictionaries");
|
|
338
|
+
var scope = ctx.settingsScope.bind({ namespace: "plugin-marketplace" });
|
|
339
|
+
var api = ctx.connection.api;
|
|
249
340
|
ctx.slots.inject("settings.section", function () {
|
|
250
341
|
return ctx.slots.register({
|
|
251
342
|
name: "settings.section",
|
|
@@ -253,7 +344,9 @@ window.__ModuleLoader__.load({
|
|
|
253
344
|
order: 20,
|
|
254
345
|
label: function () { return t("nav"); },
|
|
255
346
|
locale: NS
|
|
256
|
-
},
|
|
347
|
+
}, function (props) {
|
|
348
|
+
return h(MarketplaceSection, Object.assign({}, props, { scope: scope, api: api }));
|
|
349
|
+
});
|
|
257
350
|
});
|
|
258
351
|
}
|
|
259
352
|
|
package/index.js
CHANGED
|
@@ -1,11 +1,162 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-plugin-marketplace — node half.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Host side of the marketplace: owns a `plugin-marketplace` settings
|
|
5
|
+
* namespace used as a message channel between the browser UI and this
|
|
6
|
+
* process. The client writes an `install` request; the settings watch here
|
|
7
|
+
* spawns `dsh plugin --profile <p> add <pkg>` (npm install into the profile)
|
|
8
|
+
* and appends the mount row to the profile's cordis.patch.yml, then reports
|
|
9
|
+
* progress/result back through `installState`.
|
|
10
|
+
*
|
|
11
|
+
* The settings section UI itself lives in the browser half (exports["./client"]).
|
|
7
12
|
*/
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import z from "@deepseek-ai/schemastery";
|
|
18
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
19
|
+
import { ensureSettingsNamespaceExposed } from "./vendor/dsh-settings-expose.js";
|
|
20
|
+
|
|
21
|
+
/** Cordis plugin name. */
|
|
22
|
+
const name = "plugin-marketplace";
|
|
23
|
+
/** Settings namespace owned by this plugin (Web UI settings section + install channel). */
|
|
24
|
+
const NS = settingsNamespace("plugin-marketplace");
|
|
25
|
+
|
|
26
|
+
/** Runtime schema: the install request + the install state report. */
|
|
27
|
+
const Config = z.object({
|
|
28
|
+
install: z.object({
|
|
29
|
+
/** npm package name to install ("" = no pending request). */
|
|
30
|
+
pkg: z.string().default(""),
|
|
31
|
+
/** client-set timestamp; host dedups on it. */
|
|
32
|
+
ts: z.number().default(0),
|
|
33
|
+
}),
|
|
34
|
+
installState: z.object({
|
|
35
|
+
status: z.string().default("idle"), // idle | running | ok | error
|
|
36
|
+
message: z.string().default(""),
|
|
37
|
+
ts: z.number().default(0),
|
|
38
|
+
}),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** npm package-name shape (scope/name, no spaces, no path chars). */
|
|
42
|
+
const PKG_NAME = /^@?[a-z0-9][a-z0-9-._]*(?:\/[a-z0-9][a-z0-9-._]*)?$/;
|
|
43
|
+
|
|
44
|
+
function apply(ctx, config) {
|
|
45
|
+
let sourceGetter = null;
|
|
46
|
+
let lastTs = 0;
|
|
47
|
+
|
|
48
|
+
// ── settings-backed configuration ─────────────────────────────────────────
|
|
49
|
+
// The browser half edits this namespace; `onChange` fires on every write,
|
|
50
|
+
// which is what turns an `install` request into a real install.
|
|
51
|
+
installSettingsSection(ctx, NS, Config, config, {
|
|
52
|
+
setSource: (getter) => {
|
|
53
|
+
sourceGetter = getter;
|
|
54
|
+
},
|
|
55
|
+
onChange: () => {
|
|
56
|
+
void maybeRunInstall();
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// dsh-host-apiproxy hard-codes which namespaces the Web client may touch;
|
|
61
|
+
// patch the allowlist idempotently so the client can write install requests.
|
|
62
|
+
ensureSettingsNamespaceExposed(ctx, "plugin-marketplace", ctx.logger);
|
|
63
|
+
|
|
64
|
+
/** Resolved current config (settings layer over the entry). */
|
|
65
|
+
function current() {
|
|
66
|
+
return sourceGetter ? sourceGetter() : config;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Write the install-state report (and clear the consumed request). */
|
|
70
|
+
async function report(status, message, pkg) {
|
|
71
|
+
const settings = ctx.get("settings");
|
|
72
|
+
if (!settings) return;
|
|
73
|
+
const ts = Date.now();
|
|
74
|
+
await settings.update(NS, {
|
|
75
|
+
installState: { status, message, ts, pkg: pkg || "" },
|
|
76
|
+
install: { pkg: "", ts: 0 },
|
|
77
|
+
}).catch((error) => {
|
|
78
|
+
ctx.logger.warn(`[plugin-marketplace] state write failed: ${String(error)}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Find the active profile name from the process args ("web" default). */
|
|
83
|
+
function profileName() {
|
|
84
|
+
const argv = process.argv;
|
|
85
|
+
const i = argv.indexOf("--profile");
|
|
86
|
+
if (i >= 0 && argv[i + 1]) return argv[i + 1];
|
|
87
|
+
return "web";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The dsh CLI entry used to boot this process (its own bin.js). */
|
|
91
|
+
function dshBin() {
|
|
92
|
+
return process.argv[1];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Append a mount row for `pkg` to cordis.patch.yml (idempotent). */
|
|
96
|
+
function ensureMounted(pkg, profile) {
|
|
97
|
+
const dshHome = process.env.DSH_HOME || join(os.homedir(), ".dsh");
|
|
98
|
+
const patchPath = join(dshHome, "profiles", profile, "cordis.patch.yml");
|
|
99
|
+
if (!existsSync(patchPath)) return " (patch file not found; mount manually)";
|
|
100
|
+
const id = pkg.replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]/g, "-") || pkg;
|
|
101
|
+
let src = readFileSync(patchPath, "utf8");
|
|
102
|
+
if (src.includes(`name: '${pkg}'`) || src.includes(`name: "${pkg}"`)) return " (already mounted)";
|
|
103
|
+
// Append under the first `- insert:` list; match its indentation (4 spaces
|
|
104
|
+
// is the convention used by dsh profiles).
|
|
105
|
+
const insertAt = src.search(/^- insert:\s*$/m);
|
|
106
|
+
const row = `\n - id: ${id}\n name: '${pkg}'`;
|
|
107
|
+
if (insertAt >= 0) {
|
|
108
|
+
src = src.slice(0, insertAt + "- insert:".length) + row + src.slice(insertAt + "- insert:".length);
|
|
109
|
+
} else {
|
|
110
|
+
src += `\n- insert:${row}\n`;
|
|
111
|
+
}
|
|
112
|
+
writeFileSync(patchPath, src, "utf8");
|
|
113
|
+
return " (mounted in cordis.patch.yml)";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Run the install; resolve {code, stdout, stderr}. */
|
|
117
|
+
function runInstall(pkg, profile) {
|
|
118
|
+
const bin = dshBin();
|
|
119
|
+
const node = process.execPath;
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
execFile(node, [bin, "plugin", "--profile", profile, "add", pkg], {
|
|
122
|
+
timeout: 5 * 60 * 1000,
|
|
123
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
}, (error, stdout, stderr) => {
|
|
126
|
+
resolve({ code: error ? (error.code ?? 1) : 0, stdout, stderr });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function maybeRunInstall() {
|
|
132
|
+
try {
|
|
133
|
+
const cfg = current();
|
|
134
|
+
const req = cfg?.install;
|
|
135
|
+
if (!req || !req.pkg || req.ts === lastTs || req.ts === 0) return;
|
|
136
|
+
lastTs = req.ts;
|
|
137
|
+
const pkg = String(req.pkg).trim();
|
|
138
|
+
if (!PKG_NAME.test(pkg)) {
|
|
139
|
+
await report("error", `invalid package name: ${pkg}`, pkg);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const profile = profileName();
|
|
143
|
+
ctx.logger.info(`[plugin-marketplace] installing ${pkg} (profile=${profile})…`);
|
|
144
|
+
await report("running", `installing ${pkg}…`, pkg);
|
|
145
|
+
const result = await runInstall(pkg, profile);
|
|
146
|
+
if (result.code !== 0) {
|
|
147
|
+
const tail = (result.stderr || result.stdout || "").trim().split("\n").slice(-3).join(" ");
|
|
148
|
+
ctx.logger.warn(`[plugin-marketplace] install failed (${result.code}): ${tail}`);
|
|
149
|
+
await report("error", `install failed: ${tail || "unknown error"}`, pkg);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const mountNote = ensureMounted(pkg, profile);
|
|
153
|
+
ctx.logger.info(`[plugin-marketplace] ${pkg} installed${mountNote}`);
|
|
154
|
+
await report("ok", `${pkg} installed${mountNote}. Restart dsh web to load it.`, pkg);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
ctx.logger.warn(`[plugin-marketplace] install flow failed: ${String(error)}`);
|
|
157
|
+
await report("error", String(error instanceof Error ? error.message : error));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
10
161
|
|
|
11
|
-
export { apply };
|
|
162
|
+
export { Config, apply, name };
|
package/package.json
CHANGED
|
@@ -1,44 +1,51 @@
|
|
|
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
|
-
"dsh-plugin",
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-marketplace",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "DeepSeek Harness Web UI 内置插件市场:在设置页直接浏览 github.com/topics/dsh-plugin,支持搜索、按 Star 排序、查看安装方式。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"client": {
|
|
14
|
+
"platform": "web"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"client.js",
|
|
20
|
+
"README.md",
|
|
21
|
+
"README.zh.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"vendor"
|
|
24
|
+
],
|
|
25
|
+
"homepage": "https://github.com/Scorp1o117/dsh-plugin-marketplace",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"dsh",
|
|
28
|
+
"dsh-plugin",
|
|
29
|
+
"deepseek-harness",
|
|
30
|
+
"marketplace",
|
|
31
|
+
"web-ui"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": "\u003e=20.18"
|
|
36
|
+
},
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/Scorp1o117/dsh-plugin-marketplace.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/Scorp1o117/dsh-plugin-marketplace/issues"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": "^18.2.0",
|
|
46
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-host-apiproxy exposes only namespaces listed in its hard-coded
|
|
3
|
+
* WEB_SETTINGS_NAMESPACES allowlist to the Web settings client. A namespace
|
|
4
|
+
* registered by a third-party plugin answers `settings-not-exposed` even
|
|
5
|
+
* though it is registered — upstream explicitly defers letting plugins
|
|
6
|
+
* expose their own configuration.
|
|
7
|
+
*
|
|
8
|
+
* This module idempotently patches that allowlist in the dsh installation
|
|
9
|
+
* actually loaded by the host process, so the plugin's settings section
|
|
10
|
+
* becomes visible in the Web UI without manual edits. A dsh update
|
|
11
|
+
* overwrites the file; the next plugin start re-patches it.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { dirname, join, sep } from "node:path";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ensure `nsName` is present in dsh-host-apiproxy's WEB_SETTINGS_NAMESPACES.
|
|
19
|
+
* No-op when already exposed or when the file cannot be located/patched.
|
|
20
|
+
* @param {import("cordis").Context} ctx
|
|
21
|
+
* @param {string} nsName - settings namespace short name (e.g. "tdai-memory").
|
|
22
|
+
* @param {{info?: Function, warn?: Function}} logger - dsh logger.
|
|
23
|
+
*/
|
|
24
|
+
export function ensureSettingsNamespaceExposed(ctx, nsName, logger) {
|
|
25
|
+
void ctx;
|
|
26
|
+
try {
|
|
27
|
+
const target = findApiproxyIndex();
|
|
28
|
+
if (!target) {
|
|
29
|
+
logger?.warn?.(`[settings-expose] could not locate dsh-host-apiproxy; add "${nsName}" to WEB_SETTINGS_NAMESPACES in dsh-host-apiproxy/lib/index.js to get a Web settings section`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let src;
|
|
33
|
+
try {
|
|
34
|
+
src = readFileSync(target, "utf8");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
logger?.warn?.(`[settings-expose] cannot read ${target}: ${String(error)}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const body = src.match(/const WEB_SETTINGS_NAMESPACES = \[([\s\S]*?)\];/)?.[1] ?? "";
|
|
40
|
+
if (body.includes(`"${nsName}"`)) return; // already exposed (manual or previous patch)
|
|
41
|
+
const patched = src.replace(/(const WEB_SETTINGS_NAMESPACES = \[[\s\S]*?)(\n\s*\];)/, (_, pre, post) => {
|
|
42
|
+
const trailingComma = /,\s*$/.test(pre) ? "" : ",";
|
|
43
|
+
const sep2 = pre.trimEnd().endsWith("[") ? "" : trailingComma;
|
|
44
|
+
return `${pre}${sep2}\n\t"${nsName}"${post}`;
|
|
45
|
+
});
|
|
46
|
+
if (patched === src) {
|
|
47
|
+
logger?.warn?.(`[settings-expose] allowlist pattern not found in ${target}; add "${nsName}" to WEB_SETTINGS_NAMESPACES manually`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
writeFileSync(target, patched, "utf8");
|
|
51
|
+
logger?.info?.(`[settings-expose] added "${nsName}" to WEB_SETTINGS_NAMESPACES (${target}) — restart dsh web for the settings section to appear`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger?.warn?.(`[settings-expose] failed: ${String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findApiproxyIndex() {
|
|
58
|
+
// 1) The host process has already loaded dsh-host-apiproxy: read the real
|
|
59
|
+
// module path from the CommonJS module cache (any install layout).
|
|
60
|
+
try {
|
|
61
|
+
const Module = createRequire(import.meta.url)("module");
|
|
62
|
+
const cache = Module._cache ?? {};
|
|
63
|
+
for (const key of Object.keys(cache)) {
|
|
64
|
+
if (key.includes(`${sep}dsh-host-apiproxy${sep}`) && key.endsWith(`${sep}index.js`)) return key;
|
|
65
|
+
}
|
|
66
|
+
} catch { /* fall through */ }
|
|
67
|
+
// 2) Fallback: sibling of @deepseek-ai/dsh-settings (dsh's nested layout).
|
|
68
|
+
try {
|
|
69
|
+
const require = createRequire(import.meta.url);
|
|
70
|
+
const settingsEntry = require.resolve("@deepseek-ai/dsh-settings");
|
|
71
|
+
const candidate = join(dirname(dirname(dirname(settingsEntry))), "dsh-host-apiproxy", "lib", "index.js");
|
|
72
|
+
if (existsSync(candidate)) return candidate;
|
|
73
|
+
} catch { /* fall through */ }
|
|
74
|
+
return "";
|
|
75
|
+
}
|