dsh-plugin-marketplace 0.4.0 → 0.4.1
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 +7 -0
- package/README.zh.md +7 -0
- package/client.js +27 -1
- package/index.js +267 -303
- package/package.json +9 -4
- package/runtime.js +82 -0
package/README.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# dsh-plugin-marketplace
|
|
2
2
|
|
|
3
|
+
## v0.4.1: desktop request handling
|
|
4
|
+
|
|
5
|
+
- Installation targets the active Host profile and home, resolves the Desktop Host `cli.js`, and runs Electron with `ELECTRON_RUN_AS_NODE=1` and `--expose-internals`.
|
|
6
|
+
- Install and AI-explain requests start outside the settings/HMR transaction. Transient state-write refusals are retried; spawn failures return an installation error.
|
|
7
|
+
- The UI shows progress while awaiting Host acknowledgement, and an error after 15 seconds without acknowledgement. This does not cap installation or model response time.
|
|
8
|
+
- Update to this release and restart DSH. Repositories without a matching npm package still return an explicit installation error.
|
|
9
|
+
|
|
3
10
|
## Configuration page (DSH 0.2.0-rc.2 and later)
|
|
4
11
|
|
|
5
12
|
Open **Plugins → Installed → dsh-plugin-marketplace** from the homepage sidebar to configure and save this plugin. The page uses the official `plugins.bundle.config` interface, without a duplicate entry in global Settings. Web and Desktop share the page. This version requires DSH 0.2.0-rc.2 or a later 0.2.x host; existing configuration is retained.
|
package/README.zh.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# dsh-plugin-marketplace
|
|
2
2
|
|
|
3
|
+
## v0.4.1:桌面端请求闭环
|
|
4
|
+
|
|
5
|
+
- 一键安装使用当前 Host 的 profile 和数据目录,解析桌面 Host 的 `cli.js`;Electron 子进程启用 `ELECTRON_RUN_AS_NODE=1` 与 `--expose-internals`。
|
|
6
|
+
- 安装和 AI 解释请求延后到 settings/HMR 事务之外处理,状态回写遇到暂时拒绝会重试,启动进程失败会返回安装错误。
|
|
7
|
+
- UI 在等待宿主确认时显示进度;15 秒未确认会显示错误。此超时只针对请求确认,不限制实际安装或模型响应时间。
|
|
8
|
+
- 需要更新到本版本并重启 DSH。GitHub 仓库没有同名 npm 包时,安装仍会明确报错。
|
|
9
|
+
|
|
3
10
|
## 配置入口(DSH 0.2.0-rc.2 起)
|
|
4
11
|
|
|
5
12
|
在首页侧边栏打开 **插件 → 已安装 → dsh-plugin-marketplace**,直接在插件详情页配置并保存。配置页注册到官方的 `plugins.bundle.config` 接口;全局设置页不再重复显示配置入口。Web 与桌面版使用相同界面,本版要求 DSH 0.2.0-rc.2 或更新的 0.2.x 版本。现有配置无需迁移。
|
package/client.js
CHANGED
|
@@ -78,6 +78,7 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
installOk: "已安装:{msg}",
|
|
79
79
|
installErr: "安装失败:{msg}",
|
|
80
80
|
installIdle: "",
|
|
81
|
+
hostTimeout: "宿主在 15 秒内没有确认请求,请检查插件是否启用并重启 DSH 后重试。",
|
|
81
82
|
installingHint: "正在后台执行 dsh plugin add,请稍候…",
|
|
82
83
|
aiExplain: "🤖 AI 解释",
|
|
83
84
|
aiExplaining: "AI 解释中,请稍候…",
|
|
@@ -109,6 +110,7 @@ window.__ModuleLoader__.load({
|
|
|
109
110
|
installOk: "Installed: {msg}",
|
|
110
111
|
installErr: "Install failed: {msg}",
|
|
111
112
|
installIdle: "",
|
|
113
|
+
hostTimeout: "The Host did not acknowledge the request within 15 seconds. Check that the plugin is enabled and restart DSH before retrying.",
|
|
112
114
|
installingHint: "Running dsh plugin add in the background…",
|
|
113
115
|
aiExplain: "🤖 AI Explain",
|
|
114
116
|
aiExplaining: "AI is explaining…",
|
|
@@ -337,6 +339,30 @@ window.__ModuleLoader__.load({
|
|
|
337
339
|
var un = typeof scope.subscribe === "function" ? scope.subscribe(sync) : null;
|
|
338
340
|
return function () { alive = false; if (un) un(); if (scope.dispose) scope.dispose(); };
|
|
339
341
|
}, [scope]);
|
|
342
|
+
var channel = installState && installState.status === "ready" ? installState.value : {};
|
|
343
|
+
var installRequest = channel.install || {};
|
|
344
|
+
var explainRequest = channel.aiExplain || {};
|
|
345
|
+
react.useEffect(function () {
|
|
346
|
+
if (!installRequest.pkg || !installRequest.ts) return;
|
|
347
|
+
var timer = setTimeout(function () {
|
|
348
|
+
set(function (prev) { return Object.assign({}, prev, { installError: t("hostTimeout") }); });
|
|
349
|
+
}, 15000);
|
|
350
|
+
return function () { clearTimeout(timer); };
|
|
351
|
+
}, [installRequest.pkg, installRequest.ts, t]);
|
|
352
|
+
react.useEffect(function () {
|
|
353
|
+
if (!explainRequest.repo || !explainRequest.ts) return;
|
|
354
|
+
var timer = setTimeout(function () {
|
|
355
|
+
set(function (prev) { return Object.assign({}, prev, { explainError: t("hostTimeout") }); });
|
|
356
|
+
}, 15000);
|
|
357
|
+
return function () { clearTimeout(timer); };
|
|
358
|
+
}, [explainRequest.repo, explainRequest.ts, t]);
|
|
359
|
+
// Pending requests immediately show progress, before the Host acknowledges.
|
|
360
|
+
var visibleInstall = installRequest.pkg ? {
|
|
361
|
+
status: s.installError ? "error" : "running", pkg: installRequest.pkg, message: s.installError || ""
|
|
362
|
+
} : channel.installState;
|
|
363
|
+
var visibleExplain = explainRequest.repo ? {
|
|
364
|
+
status: s.explainError ? "error" : "running", repo: explainRequest.repo, text: s.explainError || ""
|
|
365
|
+
} : channel.aiExplainResult;
|
|
340
366
|
// Older hosts can still return settings-not-exposed. Recommend upgrading
|
|
341
367
|
// instead of mutating the installed DSH package on disk.
|
|
342
368
|
var mutateError = function (detail, fallback) {
|
|
@@ -425,7 +451,7 @@ window.__ModuleLoader__.load({
|
|
|
425
451
|
var open = s.open && s.open.fullName === p.fullName;
|
|
426
452
|
return h("div", { key: p.fullName, className: "__mp_item" },
|
|
427
453
|
h(PluginCard, { plugin: p, t: t, onOpen: function () { openDetail(p); } }),
|
|
428
|
-
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:
|
|
454
|
+
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: visibleInstall, onInstall: onInstall, ghError: t("ghError"), explainState: visibleExplain, explainError: s.explainError, onExplain: onExplain, bundleInfo: s.bundleInfo }) : null
|
|
429
455
|
);
|
|
430
456
|
})
|
|
431
457
|
),
|
package/index.js
CHANGED
|
@@ -1,314 +1,278 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-plugin-marketplace — node half.
|
|
3
|
-
*
|
|
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"]).
|
|
12
|
-
*/
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import
|
|
17
|
-
import z from "@deepseek-ai/schemastery";
|
|
18
|
-
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
19
|
-
|
|
20
|
-
/** Cordis plugin name. */
|
|
21
|
-
const name = "plugin-marketplace";
|
|
22
|
-
/** Services this plugin needs injected from the host tree (ctx.get requires inject). */
|
|
23
|
-
const inject = ["llm", "agentDefaultModel"];
|
|
24
|
-
/** Settings namespace owned by this plugin (Web UI settings section + install channel). */
|
|
25
|
-
const NS = "plugin-marketplace";
|
|
26
|
-
|
|
27
|
-
/** Runtime schema: the install request + the install state report. */
|
|
28
|
-
const Config = z.object({
|
|
29
|
-
install: z.object({
|
|
30
|
-
/** npm package name to install ("" = no pending request). */
|
|
31
|
-
pkg: z.string().default(""),
|
|
32
|
-
/** client-set timestamp; host dedups on it. */
|
|
33
|
-
ts: z.number().default(0),
|
|
34
|
-
}),
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-marketplace — node half.
|
|
3
|
+
*
|
|
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"]).
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { AsyncResource } from "node:async_hooks";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { activeProfile, runInstall, writeState } from "./runtime.js";
|
|
17
|
+
import z from "@deepseek-ai/schemastery";
|
|
18
|
+
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
19
|
+
|
|
20
|
+
/** Cordis plugin name. */
|
|
21
|
+
const name = "plugin-marketplace";
|
|
22
|
+
/** Services this plugin needs injected from the host tree (ctx.get requires inject). */
|
|
23
|
+
const inject = ["settings", "profileContext", "llm", "agentDefaultModel"];
|
|
24
|
+
/** Settings namespace owned by this plugin (Web UI settings section + install channel). */
|
|
25
|
+
const NS = "plugin-marketplace";
|
|
26
|
+
|
|
27
|
+
/** Runtime schema: the install request + the install state report. */
|
|
28
|
+
const Config = z.object({
|
|
29
|
+
install: z.object({
|
|
30
|
+
/** npm package name to install ("" = no pending request). */
|
|
31
|
+
pkg: z.string().default(""),
|
|
32
|
+
/** client-set timestamp; host dedups on it. */
|
|
33
|
+
ts: z.number().default(0),
|
|
34
|
+
}),
|
|
35
35
|
installState: z.object({
|
|
36
36
|
status: z.string().default("idle"), // idle | running | ok | error
|
|
37
37
|
message: z.string().default(""),
|
|
38
38
|
pkg: z.string().default(""),
|
|
39
39
|
ts: z.number().default(0),
|
|
40
|
-
}),
|
|
41
|
-
/** AI-explain request (client writes repo/desc/readme, host answers). */
|
|
42
|
-
aiExplain: z.object({
|
|
43
|
-
repo: z.string().default(""),
|
|
44
|
-
desc: z.string().default(""),
|
|
45
|
-
readme: z.string().default(""),
|
|
46
|
-
ts: z.number().default(0),
|
|
47
|
-
}),
|
|
48
|
-
aiExplainResult: z.object({
|
|
49
|
-
status: z.string().default("idle"), // idle | running | ok | error
|
|
50
|
-
text: z.string().default(""),
|
|
51
|
-
/** repo this result belongs to ("owner/name"); client gates display on it. */
|
|
52
|
-
repo: z.string().default(""),
|
|
53
|
-
ts: z.number().default(0),
|
|
54
|
-
}),
|
|
40
|
+
}),
|
|
41
|
+
/** AI-explain request (client writes repo/desc/readme, host answers). */
|
|
42
|
+
aiExplain: z.object({
|
|
43
|
+
repo: z.string().default(""),
|
|
44
|
+
desc: z.string().default(""),
|
|
45
|
+
readme: z.string().default(""),
|
|
46
|
+
ts: z.number().default(0),
|
|
47
|
+
}),
|
|
48
|
+
aiExplainResult: z.object({
|
|
49
|
+
status: z.string().default("idle"), // idle | running | ok | error
|
|
50
|
+
text: z.string().default(""),
|
|
51
|
+
/** repo this result belongs to ("owner/name"); client gates display on it. */
|
|
52
|
+
repo: z.string().default(""),
|
|
53
|
+
ts: z.number().default(0),
|
|
54
|
+
}),
|
|
55
55
|
}).volatile();
|
|
56
|
-
|
|
57
|
-
/** npm package-name shape (scope/name, no spaces, no path chars). */
|
|
58
|
-
const PKG_NAME = /^@?[a-z0-9][a-z0-9-._]*(?:\/[a-z0-9][a-z0-9-._]*)?$/;
|
|
59
|
-
|
|
56
|
+
|
|
57
|
+
/** npm package-name shape (scope/name, no spaces, no path chars). */
|
|
58
|
+
const PKG_NAME = /^@?[a-z0-9][a-z0-9-._]*(?:\/[a-z0-9][a-z0-9-._]*)?$/;
|
|
59
|
+
|
|
60
60
|
function apply(ctx, config) {
|
|
61
61
|
const sourceGetter = () => typeof config.get === "function" ? config.get() : config;
|
|
62
|
-
let lastTs = 0;
|
|
63
|
-
let lastExplainTs = 0;
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
let lastTs = 0;
|
|
63
|
+
let lastExplainTs = 0;
|
|
64
|
+
|
|
65
|
+
// settings/document-updated may fire inside the Host's HMR transaction.
|
|
66
|
+
// Start consumption on the next task, and cancel scheduled work on unload.
|
|
67
|
+
let scheduled;
|
|
68
|
+
let disposed = false;
|
|
69
|
+
// Capture the plugin's startup context. A timer created by an HMR event
|
|
70
|
+
// inherits its AsyncLocalStorage transaction even after that transaction
|
|
71
|
+
// settles; consume requests in this resource's context instead.
|
|
72
|
+
const consumer = new AsyncResource("plugin-marketplace-requests");
|
|
73
|
+
function scheduleRequests() {
|
|
74
|
+
if (disposed || scheduled !== undefined) return;
|
|
75
|
+
scheduled = setTimeout(() => {
|
|
76
|
+
scheduled = undefined;
|
|
77
|
+
consumer.runInAsyncScope(() => {
|
|
78
|
+
void maybeRunInstall();
|
|
79
|
+
void maybeRunExplain();
|
|
80
|
+
});
|
|
81
|
+
}, 0);
|
|
82
|
+
}
|
|
75
83
|
ctx.on("settings/document-updated", (id) => {
|
|
76
|
-
if (id
|
|
77
|
-
void maybeRunInstall();
|
|
78
|
-
void maybeRunExplain();
|
|
79
|
-
});
|
|
80
|
-
queueMicrotask(() => {
|
|
81
|
-
void maybeRunInstall();
|
|
82
|
-
void maybeRunExplain();
|
|
84
|
+
if (id === NS) scheduleRequests();
|
|
83
85
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
/** Resolved current config (settings layer over the entry). */
|
|
88
|
-
function current() {
|
|
86
|
+
ctx.effect(() => () => { disposed = true; clearTimeout(scheduled); consumer.emitDestroy(); });
|
|
87
|
+
scheduleRequests();
|
|
88
|
+
|
|
89
|
+
/** Resolved current config (settings layer over the entry). */
|
|
90
|
+
function current() {
|
|
89
91
|
return sourceGetter();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Write the install-state report (and clear the consumed request). */
|
|
93
|
-
async function report(status, message, pkg) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
async function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const prompt =
|
|
278
|
-
`插件仓库:${repo}\n` +
|
|
279
|
-
(desc ? `简介:${desc}\n` : "") +
|
|
280
|
-
(readme ? `README 摘要:\n${readme}\n` : "") +
|
|
281
|
-
`\n请用简体中文、3~5 句话直接告诉我这个插件大概是干嘛的(核心用途、解决什么问题、适合谁)。不要复述仓库名,不要罗列安装步骤。`;
|
|
282
|
-
await reportExplain("running", "", repo);
|
|
283
|
-
ctx.logger.info(`[plugin-marketplace] explaining ${repo} (model=${route ? `${route.provider}/${route.model}` : "default"})…`);
|
|
284
|
-
const messages = [createUserMessage({
|
|
285
|
-
content: [{ type: "text", text: prompt }],
|
|
286
|
-
source: { kind: "plugin", plugin: "dsh-plugin-marketplace" },
|
|
287
|
-
})];
|
|
288
|
-
const options = {
|
|
289
|
-
messages,
|
|
290
|
-
maxTokens: 400,
|
|
291
|
-
purpose: "plugin-marketplace-explain",
|
|
292
|
-
};
|
|
293
|
-
if (route) {
|
|
294
|
-
options.provider = route.provider;
|
|
295
|
-
options.model = route.model;
|
|
296
|
-
}
|
|
297
|
-
const assembler = new BlockAssembler();
|
|
298
|
-
for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk);
|
|
299
|
-
const blocks = assembler.blocks();
|
|
300
|
-
const text = blocks
|
|
301
|
-
.filter((block) => block.type === "text")
|
|
302
|
-
.map((block) => block.text)
|
|
303
|
-
.join(" ")
|
|
304
|
-
.trim();
|
|
305
|
-
if (!text) throw new Error("model produced no text");
|
|
306
|
-
await reportExplain("ok", text, repo);
|
|
307
|
-
} catch (error) {
|
|
308
|
-
ctx.logger.warn(`[plugin-marketplace] explain flow failed: ${String(error)}`);
|
|
309
|
-
await reportExplain("error", String(error instanceof Error ? error.message : error), repo);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export { Config, apply, inject, name };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Write the install-state report (and clear the consumed request). */
|
|
95
|
+
async function report(status, message, pkg) {
|
|
96
|
+
await writeState(ctx.get("settings"), NS, {
|
|
97
|
+
installState: { status, message, ts: Date.now(), pkg: pkg || "" },
|
|
98
|
+
install: { pkg: "", ts: 0 },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Read the profile manifest (its `dsh.profile.bundles` list), or null. */
|
|
103
|
+
function profileManifest(profile) {
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(readFileSync(join(profile.dir, "package.json"), "utf8"));
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Whether the installed package declares dsh.bundle.patch (auto-mounted via its bundle layer). */
|
|
112
|
+
function isBundlePackage(pkg, profile) {
|
|
113
|
+
try {
|
|
114
|
+
const manifest = JSON.parse(readFileSync(join(profile.dir, "node_modules", pkg, "package.json"), "utf8"));
|
|
115
|
+
return typeof manifest?.dsh?.bundle?.patch === "string" && manifest.dsh.bundle.patch.length > 0;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Append a mount row for `pkg` to cordis.patch.yml (idempotent).
|
|
123
|
+
* Bundle plugins (already in `dsh.profile.bundles`, or whose installed
|
|
124
|
+
* package declares `dsh.bundle.patch`) are mounted by their own bundle
|
|
125
|
+
* layer — adding a manual row would create a duplicate loader entry and
|
|
126
|
+
* abort boot. A STALE manual row left by an earlier non-bundle version is
|
|
127
|
+
* removed instead, so reinstalling a bundle plugin never breaks boot.
|
|
128
|
+
*/
|
|
129
|
+
function ensureMounted(pkg, profile) {
|
|
130
|
+
const patchPath = join(profile.dir, "cordis.patch.yml");
|
|
131
|
+
if (!existsSync(patchPath)) return " (patch file not found; mount manually)";
|
|
132
|
+
const bundles = profileManifest(profile)?.dsh?.profile?.bundles ?? [];
|
|
133
|
+
if (bundles.includes(pkg) || isBundlePackage(pkg, profile)) {
|
|
134
|
+
// Bundle layer auto-mounts; remove any stale manual row (pre-bundle).
|
|
135
|
+
const src = readFileSync(patchPath, "utf8");
|
|
136
|
+
const cleaned = src.replace(new RegExp(`[ \\t]*- id: [^\\n]*\\n[ \\t]*name: ['"]${pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]\\n?`, "g"), "");
|
|
137
|
+
if (cleaned !== src) {
|
|
138
|
+
writeFileSync(patchPath, cleaned, "utf8");
|
|
139
|
+
return " (removed stale manual mount row; bundle layer auto-mounts)";
|
|
140
|
+
}
|
|
141
|
+
return bundles.includes(pkg) ? " (already mounted via dsh.profile.bundles)" : " (bundle plugin: auto-mounted via its own dsh.bundle layer)";
|
|
142
|
+
}
|
|
143
|
+
const id = pkg.replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]/g, "-") || pkg;
|
|
144
|
+
let src = readFileSync(patchPath, "utf8");
|
|
145
|
+
if (src.includes(`name: '${pkg}'`) || src.includes(`name: "${pkg}"`)) return " (already mounted)";
|
|
146
|
+
const row = `\n - id: ${id}\n name: '${pkg}'`;
|
|
147
|
+
// A flow-style empty array (`[]`, optionally after comment lines) is a
|
|
148
|
+
// valid empty patch: replace it with the block-style insert list.
|
|
149
|
+
// Appending after `[]` would produce two top-level documents in one
|
|
150
|
+
// stream and crash boot with a YAML parse error.
|
|
151
|
+
const emptyArray = /^(\s*(?:#[^\n]*\n?)*)\[\s*\]\s*$/.exec(src);
|
|
152
|
+
if (emptyArray) {
|
|
153
|
+
writeFileSync(patchPath, `${emptyArray[1]}- insert:${row}\n`, "utf8");
|
|
154
|
+
return " (mounted in cordis.patch.yml)";
|
|
155
|
+
}
|
|
156
|
+
// Insert under the first `- insert:` list; match its indentation (4
|
|
157
|
+
// spaces is the convention used by dsh profiles).
|
|
158
|
+
const insertAt = src.search(/^- insert:\s*$/m);
|
|
159
|
+
if (insertAt >= 0) {
|
|
160
|
+
src = src.slice(0, insertAt + "- insert:".length) + row + src.slice(insertAt + "- insert:".length);
|
|
161
|
+
} else {
|
|
162
|
+
// A non-empty flow-style array cannot be safely extended with string
|
|
163
|
+
// surgery — refuse rather than corrupt the file.
|
|
164
|
+
if (/^\s*\[/.test(src)) return " (patch file uses a flow-style array; mount manually)";
|
|
165
|
+
src += `\n- insert:${row}\n`;
|
|
166
|
+
}
|
|
167
|
+
writeFileSync(patchPath, src, "utf8");
|
|
168
|
+
return " (mounted in cordis.patch.yml)";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function maybeRunInstall() {
|
|
172
|
+
let pkg = "";
|
|
173
|
+
try {
|
|
174
|
+
const cfg = current();
|
|
175
|
+
const req = cfg?.install;
|
|
176
|
+
if (!req || !req.pkg || req.ts === lastTs || req.ts === 0) return;
|
|
177
|
+
lastTs = req.ts;
|
|
178
|
+
pkg = String(req.pkg).trim();
|
|
179
|
+
if (!PKG_NAME.test(pkg)) {
|
|
180
|
+
await report("error", `invalid package name: ${pkg}`, pkg);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const profile = activeProfile(ctx);
|
|
184
|
+
ctx.logger.info(`[plugin-marketplace] installing ${pkg} (profile=${profile.name})…`);
|
|
185
|
+
await report("running", `installing ${pkg}…`, pkg);
|
|
186
|
+
const result = await runInstall(pkg, profile);
|
|
187
|
+
if (result.code !== 0) {
|
|
188
|
+
const output = (result.stderr || result.stdout || "").trim();
|
|
189
|
+
const tail = output.includes("ERR_PNPM_IGNORED_BUILDS")
|
|
190
|
+
? "dependency build scripts were blocked by pnpm; the plugin author must make native build dependencies opt-in"
|
|
191
|
+
: output.split("\n").slice(-3).join(" ");
|
|
192
|
+
ctx.logger.warn(`[plugin-marketplace] install failed (${result.code}): ${tail}`);
|
|
193
|
+
await report("error", `install failed: ${tail || "unknown error"}`, pkg);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const mountNote = ensureMounted(pkg, profile);
|
|
197
|
+
ctx.logger.info(`[plugin-marketplace] ${pkg} installed${mountNote}`);
|
|
198
|
+
await report("ok", `${pkg} installed${mountNote}. Restart DSH to load it.`, pkg);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
ctx.logger.warn(`[plugin-marketplace] install flow failed: ${String(error)}`);
|
|
201
|
+
await report("error", String(error instanceof Error ? error.message : error), pkg)
|
|
202
|
+
.catch((writeError) => ctx.logger.warn(`[plugin-marketplace] error state write failed: ${String(writeError)}`));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Write the AI-explain result report (and clear the consumed request). */
|
|
207
|
+
async function reportExplain(status, text, repo) {
|
|
208
|
+
await writeState(ctx.get("settings"), NS, {
|
|
209
|
+
aiExplainResult: { status, text, repo: repo || "", ts: Date.now() },
|
|
210
|
+
aiExplain: { repo: "", desc: "", readme: "", ts: 0 },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Answer an aiExplain request with the deployment's default model: tell the
|
|
216
|
+
* user in plain Chinese what this plugin roughly does. Uses the same
|
|
217
|
+
* settings-backed message channel as the install flow.
|
|
218
|
+
*/
|
|
219
|
+
async function maybeRunExplain() {
|
|
220
|
+
let repo = "";
|
|
221
|
+
try {
|
|
222
|
+
const cfg = current();
|
|
223
|
+
const req = cfg?.aiExplain;
|
|
224
|
+
if (!req || !req.repo || req.ts === lastExplainTs || req.ts === 0) return;
|
|
225
|
+
lastExplainTs = req.ts;
|
|
226
|
+
repo = String(req.repo);
|
|
227
|
+
const llm = ctx.llm;
|
|
228
|
+
if (!llm) {
|
|
229
|
+
await reportExplain("error", "LLM service unavailable; configure a model in Settings → Models.", repo);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Route through the deployment's default model when one is configured.
|
|
233
|
+
let route = null;
|
|
234
|
+
try {
|
|
235
|
+
const defaults = ctx.agentDefaultModel?.currentSelection?.();
|
|
236
|
+
if (defaults?.provider && defaults?.model) route = defaults;
|
|
237
|
+
} catch { /* no default model service; fall back to the adapter default */ }
|
|
238
|
+
const desc = String(req.desc || "").trim();
|
|
239
|
+
const readme = String(req.readme || "").trim().slice(0, 1500);
|
|
240
|
+
const prompt =
|
|
241
|
+
`插件仓库:${repo}\n` +
|
|
242
|
+
(desc ? `简介:${desc}\n` : "") +
|
|
243
|
+
(readme ? `README 摘要:\n${readme}\n` : "") +
|
|
244
|
+
`\n请用简体中文、3~5 句话直接告诉我这个插件大概是干嘛的(核心用途、解决什么问题、适合谁)。不要复述仓库名,不要罗列安装步骤。`;
|
|
245
|
+
await reportExplain("running", "", repo);
|
|
246
|
+
ctx.logger.info(`[plugin-marketplace] explaining ${repo} (model=${route ? `${route.provider}/${route.model}` : "default"})…`);
|
|
247
|
+
const messages = [createUserMessage({
|
|
248
|
+
content: [{ type: "text", text: prompt }],
|
|
249
|
+
source: { kind: "plugin", plugin: "dsh-plugin-marketplace" },
|
|
250
|
+
})];
|
|
251
|
+
const options = {
|
|
252
|
+
messages,
|
|
253
|
+
maxTokens: 400,
|
|
254
|
+
purpose: "plugin-marketplace-explain",
|
|
255
|
+
};
|
|
256
|
+
if (route) {
|
|
257
|
+
options.provider = route.provider;
|
|
258
|
+
options.model = route.model;
|
|
259
|
+
}
|
|
260
|
+
const assembler = new BlockAssembler();
|
|
261
|
+
for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk);
|
|
262
|
+
const blocks = assembler.blocks();
|
|
263
|
+
const text = blocks
|
|
264
|
+
.filter((block) => block.type === "text")
|
|
265
|
+
.map((block) => block.text)
|
|
266
|
+
.join(" ")
|
|
267
|
+
.trim();
|
|
268
|
+
if (!text) throw new Error("model produced no text");
|
|
269
|
+
await reportExplain("ok", text, repo);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
ctx.logger.warn(`[plugin-marketplace] explain flow failed: ${String(error)}`);
|
|
272
|
+
await reportExplain("error", String(error instanceof Error ? error.message : error), repo)
|
|
273
|
+
.catch((writeError) => ctx.logger.warn(`[plugin-marketplace] explain error state write failed: ${String(writeError)}`));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export { Config, apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-marketplace",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "DeepSeek Harness Web UI 内置插件市场:在设置页直接浏览 github.com/topics/dsh-plugin,支持搜索、按 Star 排序、查看安装方式。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -37,10 +37,11 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"test": "node --test tests/settings-write-verify.test.mjs && node --test tests/plugin-page.test.mjs"
|
|
40
|
+
"test": "node --test tests/settings-write-verify.test.mjs tests/runtime.test.mjs && node --test tests/plugin-page.test.mjs"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"index.js",
|
|
44
|
+
"runtime.js",
|
|
44
45
|
"client.js",
|
|
45
46
|
"cordis.patch.yml",
|
|
46
47
|
"README.md",
|
|
@@ -68,11 +69,15 @@
|
|
|
68
69
|
"url": "https://github.com/Scorp1o117/dsh-plugin-marketplace/issues"
|
|
69
70
|
},
|
|
70
71
|
"peerDependencies": {
|
|
71
|
-
"
|
|
72
|
+
"@deepseek-ai/dsh-llm": ">=0.2.0-rc.2 <0.3.0",
|
|
72
73
|
"@deepseek-ai/dsh-settings": ">=0.2.0-rc.2 <0.3.0",
|
|
73
|
-
"
|
|
74
|
+
"react": "^18.2.0"
|
|
74
75
|
},
|
|
75
76
|
"dependencies": {
|
|
76
77
|
"@deepseek-ai/schemastery": "^3.18.4"
|
|
78
|
+
},
|
|
79
|
+
"devDependencies": {
|
|
80
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2",
|
|
81
|
+
"@deepseek-ai/dsh-settings": "0.2.0-rc.2"
|
|
77
82
|
}
|
|
78
83
|
}
|
package/runtime.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
/** Prefer the active Host profile; never infer it from other installed profiles. */
|
|
10
|
+
function activeProfile(ctx, runtime = process) {
|
|
11
|
+
const profile = ctx.get("profileContext");
|
|
12
|
+
const document = ctx.get("settings")?.documentPath;
|
|
13
|
+
const dir = profile?.dir || (document ? dirname(document) : "");
|
|
14
|
+
if (dir) return { name: profile?.name || basename(dir), dir, home: profile?.home || dirname(dirname(dir)) };
|
|
15
|
+
const i = runtime.argv.indexOf("--profile");
|
|
16
|
+
const inline = runtime.argv.find((arg) => arg.startsWith("--profile="));
|
|
17
|
+
const name = (i >= 0 ? runtime.argv[i + 1] : "") || inline?.slice(10)
|
|
18
|
+
|| runtime.env.DSH_PROFILE || (runtime.versions?.electron ? "desktop" : "web");
|
|
19
|
+
const home = runtime.env.DSH_HOME?.trim() || join(os.homedir(), ".dsh");
|
|
20
|
+
return { name, home, dir: join(home, "profiles", name) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Desktop Host index.js is not a CLI; use its sibling cli.js instead. */
|
|
24
|
+
function cliEntry(runtime = process, exists = existsSync) {
|
|
25
|
+
const candidates = [];
|
|
26
|
+
const boot = runtime.argv[1];
|
|
27
|
+
if (typeof boot === "string") {
|
|
28
|
+
if (/[\\/]dsh-desktop-host[\\/]lib[\\/]index\.js$/.test(boot)) {
|
|
29
|
+
candidates.push(join(dirname(boot), "cli.js"));
|
|
30
|
+
} else if (/[\\/]dsh[\\/]lib[\\/]bin\.js$/.test(boot)
|
|
31
|
+
|| /[\\/]dsh-desktop-host[\\/]lib[\\/]cli\.js$/.test(boot)) candidates.push(boot);
|
|
32
|
+
}
|
|
33
|
+
if (runtime.resourcesPath) candidates.push(join(runtime.resourcesPath,
|
|
34
|
+
"app.asar", "dsh", "node_modules", "@deepseek-ai", "dsh-desktop-host", "lib", "cli.js"));
|
|
35
|
+
// In Electron's Node child resourcesPath can be absent, but execPath is still
|
|
36
|
+
// the app binary. macOS stores resources next to MacOS; Windows/Linux beside it.
|
|
37
|
+
if (runtime.versions?.electron) {
|
|
38
|
+
for (const resources of [join(dirname(runtime.execPath), "resources"), join(dirname(runtime.execPath), "..", "Resources")]) {
|
|
39
|
+
candidates.push(join(resources, "app.asar", "dsh", "node_modules", "@deepseek-ai", "dsh-desktop-host", "lib", "cli.js"));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!runtime.versions?.electron) {
|
|
43
|
+
try { candidates.push(join(dirname(require.resolve("@deepseek-ai/dsh/package.json")), "lib", "bin.js")); } catch { /* optional CLI */ }
|
|
44
|
+
}
|
|
45
|
+
return candidates.find((candidate) => exists(candidate));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function runInstall(pkg, profile, runtime = process, execute = execFile, exists = existsSync) {
|
|
49
|
+
const bin = cliEntry(runtime, exists);
|
|
50
|
+
if (!bin) return Promise.resolve({ code: 1, stdout: "", stderr: "cannot locate the dsh CLI entry" });
|
|
51
|
+
const args = [...(runtime.versions?.electron ? ["--expose-internals"] : []),
|
|
52
|
+
bin, "plugin", "--profile", profile.name, "add", pkg];
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
try {
|
|
55
|
+
execute(runtime.execPath, args, {
|
|
56
|
+
env: { ...runtime.env, DSH_HOME: profile.home, ELECTRON_RUN_AS_NODE: "1" },
|
|
57
|
+
timeout: 5 * 60 * 1000,
|
|
58
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
59
|
+
windowsHide: true,
|
|
60
|
+
}, (error, stdout, stderr) => resolve({ code: error ? (error.code ?? 1) : 0, stdout, stderr }));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
resolve({ code: 1, stdout: "", stderr: String(error.message || error) });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Let the current HMR transaction finish, and retry transient write refusals. */
|
|
68
|
+
async function writeState(settings, ns, patch, wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
69
|
+
if (!settings) throw new Error("settings service unavailable");
|
|
70
|
+
for (let attempt = 1; attempt <= 6; attempt++) {
|
|
71
|
+
try {
|
|
72
|
+
const result = await settings.update(ns, patch);
|
|
73
|
+
if (result === false) throw new Error("settings state write refused");
|
|
74
|
+
return;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (attempt === 6) throw error;
|
|
77
|
+
await wait(400 * attempt);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { activeProfile, cliEntry, runInstall, writeState };
|