@1e0zj/dsh-plugin-mall 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1e0zj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # dsh-plugin-mall
2
+
3
+ **dsh 插件市场** — 搜索 GitHub `dsh-plugin` 话题下的 DeepSeek Harness 插件仓库,并一键安装到本地 dsh profile。
4
+
5
+ 两个入口:
6
+
7
+ 1. **设置 → 插件 → 插件市场 tab**(浏览器 UI,dsh web 模式):搜索、看详情、点安装、看进度与已装列表
8
+ 2. **会话内 4 个 agent 工具**(所有模式可用)
9
+
10
+ | 工具 | 作用 |
11
+ |---|---|
12
+ | `market_search` | 搜索 GitHub 上带 `topic:dsh-plugin` 标签的仓库(按 star 排序,可按关键词过滤) |
13
+ | `market_info` | 查看单个仓库详情:star、license、package.json、是否声明 `dsh.bundle.patch` / `dsh.client` |
14
+ | `market_install` | 把插件装进某个 profile(后台任务,可用 `job_output` 查看进度) |
15
+ | `market_installed` | 列出某 profile 已装的插件及其 bundle 状态 |
16
+
17
+ ## 安装
18
+
19
+ ```powershell
20
+ # 从 npm(发布后)
21
+ dsh plugin --profile web add @1e0zj/dsh-plugin-mall
22
+
23
+ # 从 GitHub
24
+ dsh plugin --profile web add github:1e0zj/dsh-plugin-mall
25
+
26
+ # 本地开发(软链,改代码后重启 dsh 即生效)
27
+ dsh plugin --profile web add link:C:\path\to\dsh-plugin-mall
28
+ ```
29
+
30
+ 装完**重启 dsh**(`dsh web` 进程)后生效。
31
+
32
+ > Windows 下用 `link:` 开发本插件时,Node 会从项目的真实路径加载模块,
33
+ > 因此项目目录里必须先装一次依赖(`npm install`),裸导入才能解析;
34
+ > 通过 npm/GitHub 安装时无此要求(pnpm 会把真实拷贝装进 profile 的 node_modules)。
35
+
36
+ ## 工作原理
37
+
38
+ - 双面包(dual-face)插件:`dsh.bundle` 半边挂在 **host 平面**(profile bundle 层),
39
+ 注册 4 个 agent 工具(进 global 层,所有会话可见,与 MCP 工具同理);
40
+ `dsh.client` 半边是浏览器插件,往设置页插件区注册「插件市场」tab
41
+ (`settings.plugins.tab` slot;手写无构建,经 `window.__ModuleLoader__` 加载)。
42
+ - 浏览器 → 服务端走 Connection 服务的独立 RPC 通道 `/market`
43
+ (loopback-only,与 `/api` 通道互不干扰);页面发起的安装任务用进程内
44
+ tracker 跟踪 —— web host 层没有 job 控制器,`ctx.jobs` 无法在会话外起任务。
45
+ - `market_install` 复刻官方 `dsh plugin add` 的流程:在 profile 目录跑
46
+ `pnpm add <spec>`,成功后把声明了 `dsh.bundle.patch` 的依赖登记进
47
+ `dsh.profile.bundles`(layer 列表),声明了 `dsh.client` 的依赖自动在
48
+ profile 的 `cordis.patch.yml` 注册加载行,与官方 reconcile 逻辑一致。
49
+ - GitHub 源的插件安装时要跑 prepare 构建脚本,pnpm 默认拦截;本插件检测到
50
+ 拦截后会把包名自动合并进 profile 的 `pnpm-workspace.yaml` 的 `allowBuilds`
51
+ 并自动重试一次。
52
+ - 配置(`cordis.patch.yml` 中可改):`defaultProfile`(默认装进哪个 profile,
53
+ 默认 `web`)、`apiBase`(GitHub API 地址)、`perPageMax`(搜索单页上限)。
54
+
55
+ ## 发布
56
+
57
+ ```bash
58
+ npm publish
59
+ # 或在 GitHub 建仓库并打上 topic:dsh-plugin
60
+ ```
61
+
62
+ ## 开发说明
63
+
64
+ - 插件形态:`package.json` 里 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,
65
+ patch 文件把 `dsh-plugin-mall` 这一行 insert 进装配树;**同一行同时是
66
+ client 插件行**(`dsh.client` 声明让 client-modules 扫描并服务
67
+ `/plugins/<id>/client.js`)。
68
+ - node 半边导出具名成员 `{ name, inject, Config, apply }`,**不要** `export default`
69
+ (cordis loader 会做 `exports.default ?? exports` 解包,default 会吞掉 inject/Config);
70
+ client 半边是 `window.__ModuleLoader__.load({id, factory})`,导出 `{ apply, inject }`。
71
+ - 单测 `src/github.js`(无 harness 依赖):`node src/github.js --self-test`。
@@ -0,0 +1,13 @@
1
+ # dsh-plugin-mall bundle patch: registers the plugin marketplace row.
2
+ # Mounted at the host plane (profile bundle layer), so its tools land in the
3
+ # tools registry's global layer and every session sees them.
4
+ - insert:
5
+ - id: dsh-market
6
+ name: '@1e0zj/dsh-plugin-mall'
7
+ config:
8
+ # The profile market_install targets when the caller names none.
9
+ defaultProfile: web
10
+ # GitHub REST API base (override for self-hosted mirrors/proxies).
11
+ apiBase: https://api.github.com
12
+ # Upper bound for market_search perPage.
13
+ perPageMax: 30
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@1e0zj/dsh-plugin-mall",
3
+ "version": "0.1.0",
4
+ "description": "dsh 插件市场:搜索 GitHub dsh-plugin 话题下的插件仓库,一键安装到本地 dsh profile(agent 工具 + 设置页插件市场 tab)",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./client": "./src/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "cordis.patch.yml",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "dsh",
19
+ "dsh-plugin",
20
+ "deepseek-harness",
21
+ "plugin-marketplace",
22
+ "marketplace"
23
+ ],
24
+ "license": "MIT",
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ },
29
+ "client": {
30
+ "platform": "web",
31
+ "inject": [
32
+ "@deepseek-ai/dsh-client-connection",
33
+ "@deepseek-ai/dsh-client-ui-slots"
34
+ ]
35
+ }
36
+ },
37
+ "dependencies": {
38
+ "@deepseek-ai/dsh-app-boot": "^0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
40
+ "@deepseek-ai/schemastery": "^3.18.1",
41
+ "js-yaml": "^4.2.0"
42
+ },
43
+ "peerDependencies": {
44
+ "@deepseek-ai/cordis": "^4.0.1"
45
+ },
46
+ "repository": {
47
+ "url": "git+https://github.com/1e0zj/dsh-plugin-mall.git",
48
+ "type": "git"
49
+ }
50
+ }
package/src/client.js ADDED
@@ -0,0 +1,380 @@
1
+ // Browser half of dsh-plugin-mall: the Settings → Plugins → 插件市场 tab.
2
+ //
3
+ // Client plugin contract (see @deepseek-ai/dsh-client-modules): the bundle
4
+ // registers itself on window.__ModuleLoader__, exports a cordis-style
5
+ // { apply, inject }, and is served at /plugins/@1e0zj/dsh-plugin-mall/client.js.
6
+ // This file is hand-written ES5-ish JS on purpose — no build step — using the
7
+ // React instance the shell shares through the client module loader.
8
+ window.__ModuleLoader__.load({
9
+ id: "@1e0zj/dsh-plugin-mall",
10
+ factory: (require) => {
11
+ var module = { exports: {} };
12
+ var exports = module.exports;
13
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
14
+ var React = require("react");
15
+
16
+ // ── styles ──────────────────────────────────────────────────────────────
17
+ var css = [
18
+ ".mkt_root{display:flex;flex-direction:column;gap:12px;max-width:960px}",
19
+ ".mkt_head{display:flex;flex-direction:column;gap:2px}",
20
+ ".mkt_title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}",
21
+ ".mkt_sub{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
22
+ ".mkt_row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}",
23
+ ".mkt_input{flex:1;min-width:200px;background:var(--dsw-alias-bg-secondary,#fff);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 10px;font-size:13px;color:var(--dsw-alias-label-primary)}",
24
+ ".mkt_select{background:var(--dsw-alias-bg-secondary,#fff);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:6px 8px;font-size:13px;color:var(--dsw-alias-label-primary)}",
25
+ ".mkt_btn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-secondary,#fff);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer}",
26
+ ".mkt_btn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}",
27
+ ".mkt_btn:disabled{opacity:.55;cursor:default}",
28
+ ".mkt_btnPrimary{background:var(--dsw-alias-state-business-primary,#2b6cb0);border-color:transparent;color:#fff}",
29
+ ".mkt_btnPrimary:hover:not(:disabled){color:#fff;opacity:.92}",
30
+ ".mkt_btnDanger:hover:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}",
31
+ ".mkt_error{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}",
32
+ ".mkt_columns{display:grid;grid-template-columns:1fr 320px;gap:14px;align-items:start}",
33
+ "@media (max-width:860px){.mkt_columns{grid-template-columns:1fr}}",
34
+ ".mkt_list{display:flex;flex-direction:column;gap:10px;min-width:0}",
35
+ ".mkt_card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;background:var(--dsw-alias-bg-primary,#fff);display:flex;flex-direction:column;gap:6px;min-width:0}",
36
+ ".mkt_cardHead{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}",
37
+ ".mkt_name{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);overflow-wrap:anywhere}",
38
+ ".mkt_meta{font-size:12px;color:var(--dsw-alias-label-tertiary)}",
39
+ ".mkt_desc{font-size:12.5px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
40
+ ".mkt_cardActions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}",
41
+ ".mkt_rail{display:flex;flex-direction:column;gap:10px;min-width:0}",
42
+ ".mkt_panelTitle{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);margin:0}",
43
+ ".mkt_pre{font-family:Consolas,Monaco,monospace;font-size:11.5px;line-height:16px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-secondary,#f6f7f8);border-radius:6px;padding:8px;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-all}",
44
+ ".mkt_ok{color:var(--dsw-alias-state-success-primary,#2f855a)}",
45
+ ".mkt_badge{display:inline-block;border-radius:999px;padding:1px 8px;font-size:11px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary)}",
46
+ ".mkt_link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;cursor:pointer}",
47
+ ].join("\n");
48
+ var tagId = "@1e0zj/dsh-plugin-mall/market-tab.css";
49
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
50
+ var styleTag = document.createElement("style");
51
+ styleTag.dataset.plugin = "@1e0zj/dsh-plugin-mall";
52
+ styleTag.dataset.pluginCss = tagId;
53
+ styleTag.textContent = css;
54
+ document.head.appendChild(styleTag);
55
+ }
56
+
57
+ // ── tiny helpers ────────────────────────────────────────────────────────
58
+ var h = React.createElement;
59
+ var useState = React.useState;
60
+ var useEffect = React.useEffect;
61
+ var useRef = React.useRef;
62
+ var useCallback = React.useCallback;
63
+
64
+ function clip(text, max) {
65
+ var t = String(text == null ? "" : text).replace(/\s+/g, " ").trim();
66
+ return t.length > max ? t.slice(0, max - 1) + "…" : t;
67
+ }
68
+
69
+ function errorText(e) {
70
+ return String(e && e.message ? e.message : e);
71
+ }
72
+
73
+ function kindLabel(kind) {
74
+ if (kind === "bundle") return "宿主插件层";
75
+ if (kind === "client") return "浏览器UI插件";
76
+ if (kind === "missing") return "未解析";
77
+ return "普通依赖";
78
+ }
79
+
80
+ // ── job polling ─────────────────────────────────────────────────────────
81
+ function useJobPolling(call) {
82
+ var jobsRef = useRef({});
83
+ var _jobs = useState({});
84
+ var jobs = _jobs[0];
85
+ var setJobs = _jobs[1];
86
+ useEffect(function () {
87
+ var timer = setInterval(function () {
88
+ var current = jobsRef.current;
89
+ var running = Object.keys(current).filter(function (id) {
90
+ var job = current[id];
91
+ return job && job.status !== "completed" && job.status !== "failed" && job.status !== "killed";
92
+ });
93
+ if (running.length === 0) return;
94
+ running.forEach(function (id) {
95
+ call("job", { jobId: id }).then(function (value) {
96
+ var snapshot = value.snapshot || {};
97
+ setJobs(function (prev) {
98
+ var old = prev[id] || {};
99
+ var output = (old.output || "") + (value.output || "");
100
+ return Object.assign({}, prev, { [id]: Object.assign({}, old, {
101
+ status: snapshot.status,
102
+ detail: snapshot.detail,
103
+ output: output,
104
+ }) });
105
+ });
106
+ }).catch(function () { /* keep polling */ });
107
+ });
108
+ }, 1200);
109
+ return function () { clearInterval(timer); };
110
+ }, [call]);
111
+ var track = useCallback(function (id, spec) {
112
+ jobsRef.current = Object.assign({}, jobsRef.current, { [id]: { status: "running", spec: spec, output: "" } });
113
+ setJobs(Object.assign({}, jobsRef.current));
114
+ }, []);
115
+ return { jobs: jobs, track: track };
116
+ }
117
+
118
+ // ── repo card ───────────────────────────────────────────────────────────
119
+ function RepoCard(props) {
120
+ var item = props.item;
121
+ var installing = props.installing === true;
122
+ return h("div", { className: "mkt_card" },
123
+ h("div", { className: "mkt_cardHead" },
124
+ h("span", { className: "mkt_name" }, item.fullName),
125
+ h("span", { className: "mkt_meta" }, "★" + item.stars),
126
+ item.language ? h("span", { className: "mkt_meta" }, item.language) : null,
127
+ item.license ? h("span", { className: "mkt_meta" }, item.license) : null,
128
+ item.archived ? h("span", { className: "mkt_badge" }, "archived") : null
129
+ ),
130
+ item.description ? h("div", { className: "mkt_desc" }, clip(item.description, 180)) : null,
131
+ h("div", { className: "mkt_meta" }, "更新 " + (item.updatedAt || "").slice(0, 10)),
132
+ h("div", { className: "mkt_cardActions" },
133
+ h("button", { className: "mkt_btn mkt_btnPrimary", disabled: installing, onClick: function () { props.onInstall(item.fullName); } },
134
+ installing ? "安装中…" : "安装"),
135
+ h("button", { className: "mkt_btn", onClick: function () { props.onInfo(item.fullName); } }, "详情")
136
+ )
137
+ );
138
+ }
139
+
140
+ // ── info panel ──────────────────────────────────────────────────────────
141
+ function InfoPanel(props) {
142
+ var info = props.info;
143
+ if (!info) return null;
144
+ var data = info.data;
145
+ return h("div", { className: "mkt_card" },
146
+ h("p", { className: "mkt_panelTitle" }, "仓库详情"),
147
+ info.loading ? h("div", { className: "mkt_meta" }, "加载中…")
148
+ : info.error ? h("div", { className: "mkt_error" }, info.error)
149
+ : !data ? null
150
+ : h(React.Fragment, null,
151
+ h("div", { className: "mkt_name" }, data.meta.fullName),
152
+ h("div", { className: "mkt_desc" }, clip(data.meta.description, 240)),
153
+ h("div", { className: "mkt_meta" },
154
+ "★" + data.meta.stars + " · " + (data.meta.language || "-") + " · " + (data.meta.license || "-") + " · 分支 " + data.meta.defaultBranch),
155
+ data.packageJson == null
156
+ ? h("div", { className: "mkt_meta" }, "package.json: 未找到(可能不是 npm 插件)")
157
+ : h(React.Fragment, null,
158
+ h("div", { className: "mkt_meta" }, "package.json: " + (data.packageJson.name || "?") + "@" + (data.packageJson.version || "?")),
159
+ data.packageJson.dshBundlePatch != null
160
+ ? h("div", { className: "mkt_ok" }, "✓ dsh.bundle 宿主插件")
161
+ : data.packageJson.dshClientPlatform != null
162
+ ? h("div", { className: "mkt_ok" }, "✓ dsh.client 浏览器UI插件")
163
+ : h("div", { className: "mkt_meta" }, "⚠ 无 bundle/client 声明,装上是普通依赖")),
164
+ h("button", { className: "mkt_btn mkt_btnPrimary", onClick: function () { props.onInstall(data.meta.fullName); } }, "安装 github:" + data.meta.fullName),
165
+ h("a", { className: "mkt_link", href: data.meta.htmlUrl, target: "_blank", rel: "noreferrer" }, "打开仓库 ↗")
166
+ )
167
+ );
168
+ }
169
+
170
+ // ── jobs panel ──────────────────────────────────────────────────────────
171
+ function JobsPanel(props) {
172
+ var ids = Object.keys(props.jobs);
173
+ if (ids.length === 0) return null;
174
+ return h("div", { className: "mkt_card" },
175
+ h("p", { className: "mkt_panelTitle" }, "安装任务"),
176
+ ids.map(function (id) {
177
+ var job = props.jobs[id];
178
+ var done = job.status === "completed" || job.status === "failed" || job.status === "killed";
179
+ return h("div", { key: id, style: { display: "flex", flexDirection: "column", gap: "4px" } },
180
+ h("div", { className: "mkt_meta" },
181
+ id + " · " + clip(job.spec || "", 40) + " · " + (job.status || "running")),
182
+ job.detail ? h("div", { className: "mkt_desc" }, job.detail) : null,
183
+ done && job.status === "completed"
184
+ ? h("div", { className: "mkt_ok" }, "✓ 安装完成 — 重启 dsh 后生效")
185
+ : job.status === "failed"
186
+ ? h("div", { className: "mkt_error" }, "安装失败,见下方输出")
187
+ : null,
188
+ job.output ? h("pre", { className: "mkt_pre" }, job.output.slice(-4000)) : null
189
+ );
190
+ })
191
+ );
192
+ }
193
+
194
+ // ── installed panel ─────────────────────────────────────────────────────
195
+ function InstalledPanel(props) {
196
+ var installed = props.installed;
197
+ if (!installed) return null;
198
+ return h("div", { className: "mkt_card" },
199
+ h("p", { className: "mkt_panelTitle" }, "已装插件"),
200
+ installed.error
201
+ ? h("div", { className: "mkt_error" }, installed.error)
202
+ : installed.deps && installed.deps.length === 0
203
+ ? h("div", { className: "mkt_meta" }, "还没有装过插件")
204
+ : (installed.deps || []).map(function (dep) {
205
+ return h("div", { key: dep.name, style: { display: "flex", justifyContent: "space-between", gap: "8px" } },
206
+ h("span", { className: "mkt_desc" }, dep.name + "@" + dep.version),
207
+ h("span", { className: "mkt_badge" }, kindLabel(dep.kind)));
208
+ })
209
+ );
210
+ }
211
+
212
+ // ── the marketplace tab ─────────────────────────────────────────────────
213
+ function MarketplaceTab(props) {
214
+ var rpc = props.rpc;
215
+ var _query = useState("");
216
+ var query = _query[0];
217
+ var setQuery = _query[1];
218
+ var _sort = useState("stars");
219
+ var sort = _sort[0];
220
+ var setSort = _sort[1];
221
+ var _results = useState(null);
222
+ var results = _results[0];
223
+ var setResults = _results[1];
224
+ var _loading = useState(false);
225
+ var loading = _loading[0];
226
+ var setLoading = _loading[1];
227
+ var _error = useState(null);
228
+ var error = _error[0];
229
+ var setError = _error[1];
230
+ var _info = useState(null);
231
+ var info = _info[0];
232
+ var setInfo = _info[1];
233
+ var _installed = useState(null);
234
+ var installed = _installed[0];
235
+ var setInstalled = _installed[1];
236
+ var _installing = useState({});
237
+ var installing = _installing[0];
238
+ var setInstalling = _installing[1];
239
+
240
+ var call = useCallback(function (endpoint, payload) {
241
+ return rpc.call("/market", endpoint, payload || {}).then(function (res) {
242
+ if (!res || res.ok !== true) throw new Error((res && res.error) || "market request failed");
243
+ return res.value;
244
+ });
245
+ }, [rpc]);
246
+
247
+ var doSearch = useCallback(function () {
248
+ setLoading(true);
249
+ setError(null);
250
+ call("search", { query: query, sort: sort, perPage: 20 }).then(function (value) {
251
+ setResults(value);
252
+ }).catch(function (e) {
253
+ setError(errorText(e));
254
+ }).finally(function () {
255
+ setLoading(false);
256
+ });
257
+ }, [call, query, sort]);
258
+
259
+ var doInfo = useCallback(function (repo) {
260
+ setInfo({ repo: repo, loading: true });
261
+ call("info", { repo: repo }).then(function (value) {
262
+ setInfo({ repo: repo, data: value });
263
+ }).catch(function (e) {
264
+ setInfo({ repo: repo, error: errorText(e) });
265
+ });
266
+ }, [call]);
267
+
268
+ var polling = useJobPolling(call);
269
+ var track = polling.track;
270
+ var jobs = polling.jobs;
271
+
272
+ var doInstall = useCallback(function (repo) {
273
+ var spec = "github:" + repo;
274
+ setInstalling(function (prev) {
275
+ var next = Object.assign({}, prev, { [repo]: true });
276
+ return next;
277
+ });
278
+ setError(null);
279
+ call("install", { spec: spec }).then(function (value) {
280
+ track(value.jobId, spec);
281
+ }).catch(function (e) {
282
+ setError(errorText(e));
283
+ }).finally(function () {
284
+ setInstalling(function (prev) {
285
+ var next = Object.assign({}, prev);
286
+ delete next[repo];
287
+ return next;
288
+ });
289
+ });
290
+ }, [call, track]);
291
+
292
+ var refreshInstalled = useCallback(function () {
293
+ call("installed", {}).then(function (value) {
294
+ setInstalled(value);
295
+ }).catch(function (e) {
296
+ setInstalled({ error: errorText(e) });
297
+ });
298
+ }, [call]);
299
+
300
+ useEffect(function () {
301
+ refreshInstalled();
302
+ }, [refreshInstalled]);
303
+
304
+ return h("div", { className: "mkt_root" },
305
+ h("div", { className: "mkt_head" },
306
+ h("div", { className: "mkt_title" }, "插件市场"),
307
+ h("div", { className: "mkt_sub" }, "搜索 GitHub dsh-plugin 话题插件 · 安装后需重启 dsh 生效")
308
+ ),
309
+ h("div", { className: "mkt_row" },
310
+ h("input", {
311
+ className: "mkt_input",
312
+ placeholder: "搜索插件(留空列出最热)",
313
+ value: query,
314
+ onChange: function (e) { setQuery(e.target.value); },
315
+ onKeyDown: function (e) { if (e.key === "Enter") doSearch(); },
316
+ }),
317
+ h("select", {
318
+ className: "mkt_select",
319
+ value: sort,
320
+ onChange: function (e) { setSort(e.target.value); },
321
+ },
322
+ h("option", { value: "stars" }, "按 star"),
323
+ h("option", { value: "updated" }, "最近更新"),
324
+ h("option", { value: "forks" }, "按 fork")
325
+ ),
326
+ h("button", { className: "mkt_btn mkt_btnPrimary", disabled: loading, onClick: doSearch }, loading ? "搜索中…" : "搜索"),
327
+ h("button", { className: "mkt_btn", onClick: refreshInstalled }, "刷新已装")
328
+ ),
329
+ error ? h("div", { className: "mkt_error" }, error) : null,
330
+ h("div", { className: "mkt_columns" },
331
+ h("div", { className: "mkt_list" },
332
+ results == null
333
+ ? h("div", { className: "mkt_meta" }, "输入关键词点搜索,或直接搜索浏览最热插件。")
334
+ : results.items.length === 0
335
+ ? h("div", { className: "mkt_meta" }, "没有匹配的仓库。")
336
+ : h(React.Fragment, null,
337
+ h("div", { className: "mkt_meta" }, "共 " + results.total + " 个仓库(显示 " + results.items.length + " 个)"),
338
+ results.items.map(function (item) {
339
+ return h(RepoCard, {
340
+ key: item.fullName,
341
+ item: item,
342
+ installing: installing[item.fullName] === true,
343
+ onInstall: doInstall,
344
+ onInfo: doInfo,
345
+ });
346
+ })
347
+ )
348
+ ),
349
+ h("div", { className: "mkt_rail" },
350
+ h(InfoPanel, { info: info, onInstall: doInstall }),
351
+ h(JobsPanel, { jobs: jobs }),
352
+ h(InstalledPanel, { installed: installed })
353
+ )
354
+ )
355
+ );
356
+ }
357
+
358
+ // ── plugin body ─────────────────────────────────────────────────────────
359
+ var inject = ["slots", "connection"];
360
+ function apply(ctx) {
361
+ var rpc = ctx.get("connection").rpc;
362
+ ctx.effect(function () {
363
+ return ctx.slots.inject("settings.plugins.tab", function () {
364
+ return ctx.slots.register({
365
+ name: "settings.plugins.tab",
366
+ id: "market",
367
+ order: 30,
368
+ label: "插件市场",
369
+ inject: function () {
370
+ return { rpc: rpc };
371
+ },
372
+ }, MarketplaceTab);
373
+ });
374
+ }, "@1e0zj/dsh-plugin-mall: marketplace tab");
375
+ }
376
+ exports.apply = apply;
377
+ exports.inject = inject;
378
+ return module.exports;
379
+ },
380
+ });