dsh-plugin-discovery 0.0.0-stage → 1.0.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 +21 -0
- package/README.md +70 -3
- package/client.js +1409 -0
- package/cordis.patch.yml +10 -0
- package/docs/articles/csdn.md +59 -0
- package/docs/articles/juejin.md +52 -0
- package/docs/articles/zhihu.md +50 -0
- package/docs/images/discover-installed.jpg +0 -0
- package/docs/images/discover-search.png +0 -0
- package/docs/images/discover-status.jpg +0 -0
- package/icon.svg +5 -0
- package/index.js +528 -0
- package/locale/en.json +22 -0
- package/locale/zh.json +22 -0
- package/mark-remote.js +48 -0
- package/package.json +70 -6
- package/skill/SKILL.md +24 -0
package/index.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
4
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
|
+
import { markRemoteMethod } from "./mark-remote.js";
|
|
6
|
+
|
|
7
|
+
const USER_AGENT = "dsh-plugin-discovery/1.0";
|
|
8
|
+
|
|
9
|
+
function githubHeaders(token) {
|
|
10
|
+
const headers = {
|
|
11
|
+
Accept: "application/vnd.github+json",
|
|
12
|
+
"User-Agent": USER_AGENT,
|
|
13
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
14
|
+
};
|
|
15
|
+
if (token?.trim()) headers.Authorization = `Bearer ${token.trim()}`;
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseGithubRepoUrl(input) {
|
|
20
|
+
const trimmed = input.trim();
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(trimmed);
|
|
23
|
+
if (!/github\.com$/i.test(url.hostname.replace(/^www\./, ""))) return null;
|
|
24
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
25
|
+
if (parts.length < 2) return null;
|
|
26
|
+
const owner = parts[0];
|
|
27
|
+
const repo = parts[1].replace(/\.git$/, "");
|
|
28
|
+
return { spec: `github:${owner}/${repo}`, owner, repo };
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseNpmPackageUrl(input) {
|
|
35
|
+
const trimmed = input.trim();
|
|
36
|
+
if (/^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/i.test(trimmed)) {
|
|
37
|
+
return { spec: trimmed };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(trimmed);
|
|
41
|
+
if (!/npmjs\.com$/i.test(url.hostname.replace(/^www\./, ""))) return null;
|
|
42
|
+
const match = url.pathname.match(/^\/package\/((?:@[^/]+\/)?[^/]+)/);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
return { spec: decodeURIComponent(match[1]) };
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseGithubShortRepo(input) {
|
|
51
|
+
const trimmed = input.trim();
|
|
52
|
+
const match = trimmed.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/);
|
|
53
|
+
if (!match) return null;
|
|
54
|
+
return { spec: `github:${match[1]}/${match[2]}`, owner: match[1], repo: match[2] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveInputToSpec(input) {
|
|
58
|
+
const trimmed = input.trim();
|
|
59
|
+
if (!trimmed) return null;
|
|
60
|
+
if (/^(github:|gitlab:|bitbucket:)/i.test(trimmed)) return { spec: trimmed, source: "git" };
|
|
61
|
+
const gh = parseGithubRepoUrl(trimmed);
|
|
62
|
+
if (gh) return { ...gh, source: "github" };
|
|
63
|
+
const short = parseGithubShortRepo(trimmed);
|
|
64
|
+
if (short) return { ...short, source: "github" };
|
|
65
|
+
const npm = parseNpmPackageUrl(trimmed);
|
|
66
|
+
if (npm) return { ...npm, source: "npm" };
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function fetchJson(url, init = {}) {
|
|
71
|
+
const response = await fetch(url, init);
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
const text = await response.text().catch(() => "");
|
|
74
|
+
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
|
|
75
|
+
}
|
|
76
|
+
return response.json();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function searchNpm(query, limit) {
|
|
80
|
+
const params = new URLSearchParams({
|
|
81
|
+
text: `${query} dsh`,
|
|
82
|
+
size: String(Math.min(limit, 50)),
|
|
83
|
+
});
|
|
84
|
+
const data = await fetchJson(`https://registry.npmjs.org/-/v1/search?${params}`);
|
|
85
|
+
const items = Array.isArray(data.objects) ? data.objects : [];
|
|
86
|
+
return items.map((row) => {
|
|
87
|
+
const pkg = row.package || {};
|
|
88
|
+
return {
|
|
89
|
+
source: "npm",
|
|
90
|
+
spec: pkg.name,
|
|
91
|
+
title: pkg.name,
|
|
92
|
+
description: pkg.description || "",
|
|
93
|
+
url: pkg.links?.npm || `https://www.npmjs.com/package/${pkg.name}`,
|
|
94
|
+
stars: null,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mapGithubRepos(items) {
|
|
100
|
+
return (Array.isArray(items) ? items : []).map((repo) => ({
|
|
101
|
+
source: "github",
|
|
102
|
+
spec: `github:${repo.full_name}`,
|
|
103
|
+
title: repo.full_name,
|
|
104
|
+
description: repo.description || "",
|
|
105
|
+
url: repo.html_url,
|
|
106
|
+
stars: repo.stargazers_count ?? null,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function githubSearchOnce(q, limit, token) {
|
|
111
|
+
const params = new URLSearchParams({
|
|
112
|
+
q,
|
|
113
|
+
sort: "updated",
|
|
114
|
+
order: "desc",
|
|
115
|
+
per_page: String(Math.min(limit, 30)),
|
|
116
|
+
});
|
|
117
|
+
const data = await fetchJson(`https://api.github.com/search/repositories?${params}`, {
|
|
118
|
+
headers: githubHeaders(token),
|
|
119
|
+
});
|
|
120
|
+
return mapGithubRepos(data.items);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function searchGithub(query, limit, token) {
|
|
124
|
+
const trimmed = String(query || "").trim();
|
|
125
|
+
// 与 GitHub 网页搜索对齐:先用原关键词;再用 topic 收窄社区插件
|
|
126
|
+
const queries = trimmed
|
|
127
|
+
? [trimmed, `${trimmed} topic:dsh-plugin`, `${trimmed} topic:dsh`]
|
|
128
|
+
: ["topic:dsh-plugin", "topic:dsh"];
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
const merged = [];
|
|
131
|
+
let lastError = null;
|
|
132
|
+
for (const q of queries) {
|
|
133
|
+
if (merged.length >= limit) break;
|
|
134
|
+
try {
|
|
135
|
+
const batch = await githubSearchOnce(q, limit, token);
|
|
136
|
+
for (const row of batch) {
|
|
137
|
+
if (seen.has(row.spec)) continue;
|
|
138
|
+
seen.add(row.spec);
|
|
139
|
+
merged.push(row);
|
|
140
|
+
if (merged.length >= limit) break;
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
lastError = error;
|
|
144
|
+
// 无 token 时未认证配额很低;已有结果则返回,否则抛出给上层提示
|
|
145
|
+
if (!merged.length && /rate limit|API rate limit/i.test(String(error && error.message))) {
|
|
146
|
+
throw new Error("GitHub API 速率受限,请在插件配置里填写 githubToken,或稍后再试");
|
|
147
|
+
}
|
|
148
|
+
if (!merged.length) continue;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!merged.length && lastError) throw lastError;
|
|
153
|
+
return merged;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function cacheKey(query, source) {
|
|
157
|
+
return `${source}:${query.trim().toLowerCase()}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class PluginDiscoveryService extends TypertRemoteService {
|
|
161
|
+
static inject = ["tools", "pluginManager"];
|
|
162
|
+
static Config = z.object({
|
|
163
|
+
enabled: z.boolean().default(true),
|
|
164
|
+
searchCacheTtlMs: z.number().min(0).max(86400000).default(300000),
|
|
165
|
+
maxCandidates: z.number().min(1).max(50).default(20),
|
|
166
|
+
maxInspect: z.number().min(0).max(30).default(10),
|
|
167
|
+
githubToken: z.string().default(""),
|
|
168
|
+
openPanelOnLoad: z.boolean().default(false),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
constructor(ctx, config) {
|
|
172
|
+
super(ctx, "pluginDiscovery");
|
|
173
|
+
this.config = config;
|
|
174
|
+
this.cache = new Map();
|
|
175
|
+
this.ctx.effect(() => this.registerTools());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getFromCache(key) {
|
|
179
|
+
const hit = this.cache.get(key);
|
|
180
|
+
if (!hit) return null;
|
|
181
|
+
if (Date.now() - hit.at > this.config.searchCacheTtlMs) {
|
|
182
|
+
this.cache.delete(key);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return hit.payload;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
setCache(key, payload) {
|
|
189
|
+
this.cache.set(key, { at: Date.now(), payload });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async inspectCandidate(spec, signal) {
|
|
193
|
+
try {
|
|
194
|
+
const inspection = await this.ctx.pluginManager.inspect(spec, {}, signal);
|
|
195
|
+
if (inspection.status !== "accepted") {
|
|
196
|
+
return {
|
|
197
|
+
spec,
|
|
198
|
+
accepted: false,
|
|
199
|
+
bundle: false,
|
|
200
|
+
reason: inspection.status === "refused" ? inspection.reason : "refused",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
spec,
|
|
205
|
+
accepted: true,
|
|
206
|
+
bundle: inspection.bundle === true,
|
|
207
|
+
name: inspection.name,
|
|
208
|
+
version: inspection.version,
|
|
209
|
+
description: inspection.description,
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return {
|
|
213
|
+
spec,
|
|
214
|
+
accepted: false,
|
|
215
|
+
bundle: false,
|
|
216
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async enrichCandidates(candidates, signal) {
|
|
222
|
+
const limit = this.config.maxInspect;
|
|
223
|
+
const slice = candidates.slice(0, limit);
|
|
224
|
+
const verified = [];
|
|
225
|
+
for (const row of slice) {
|
|
226
|
+
if (signal?.aborted) break;
|
|
227
|
+
const meta = await this.inspectCandidate(row.spec, signal);
|
|
228
|
+
if (meta.bundle) {
|
|
229
|
+
verified.push({
|
|
230
|
+
...row,
|
|
231
|
+
...meta,
|
|
232
|
+
installSpec: row.spec,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return verified;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async search(query, options) {
|
|
240
|
+
if (!this.config.enabled) {
|
|
241
|
+
return { ok: false, message: "发现插件已停用", results: [], total: 0 };
|
|
242
|
+
}
|
|
243
|
+
const opts = options && typeof options === "object" ? options : {};
|
|
244
|
+
const q = String(query || "").trim() || "dsh";
|
|
245
|
+
const source = opts.source === "npm" || opts.source === "github" ? opts.source : "all";
|
|
246
|
+
const verify = opts.verify === true;
|
|
247
|
+
const key = `${cacheKey(q, source)}:v${verify ? 1 : 0}`;
|
|
248
|
+
const cached = this.getFromCache(key);
|
|
249
|
+
if (cached && !opts.skipCache) {
|
|
250
|
+
return { ok: true, cached: true, query: q, source, verify, results: cached, total: cached.length };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const limit = this.config.maxCandidates;
|
|
254
|
+
const token = opts.githubToken ?? this.config.githubToken;
|
|
255
|
+
let raw = [];
|
|
256
|
+
try {
|
|
257
|
+
if (source === "npm" || source === "all") {
|
|
258
|
+
raw = raw.concat(await searchNpm(q, limit));
|
|
259
|
+
}
|
|
260
|
+
if (source === "github" || source === "all") {
|
|
261
|
+
raw = raw.concat(await searchGithub(q, limit, token));
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
this.ctx.logger.warn("[plugin-discovery] search failed", error);
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
message: error instanceof Error ? error.message : String(error),
|
|
268
|
+
results: [],
|
|
269
|
+
total: 0,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
const deduped = [];
|
|
275
|
+
for (const row of raw) {
|
|
276
|
+
if (!row.spec || seen.has(row.spec)) continue;
|
|
277
|
+
seen.add(row.spec);
|
|
278
|
+
deduped.push({
|
|
279
|
+
...row,
|
|
280
|
+
installSpec: row.spec,
|
|
281
|
+
bundle: null,
|
|
282
|
+
});
|
|
283
|
+
if (deduped.length >= limit) break;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let results = deduped;
|
|
287
|
+
if (verify) {
|
|
288
|
+
const controller = new AbortController();
|
|
289
|
+
const timeout = setTimeout(() => controller.abort(), 120000);
|
|
290
|
+
try {
|
|
291
|
+
results = await this.enrichCandidates(deduped, controller.signal);
|
|
292
|
+
} finally {
|
|
293
|
+
clearTimeout(timeout);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
this.setCache(key, results);
|
|
298
|
+
return { ok: true, cached: false, query: q, source, verify, results, total: results.length };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async resolve(input) {
|
|
302
|
+
if (!this.config.enabled) {
|
|
303
|
+
return { ok: false, message: "发现插件已停用" };
|
|
304
|
+
}
|
|
305
|
+
const parsed = resolveInputToSpec(input);
|
|
306
|
+
if (!parsed?.spec) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
message:
|
|
310
|
+
"解析安装需要完整链接或包名(如 https://github.com/user/repo、user/repo、npm 包名)。关键词请用上方「搜索」。",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
const controller = new AbortController();
|
|
314
|
+
const timeout = setTimeout(() => controller.abort(), 90000);
|
|
315
|
+
let meta;
|
|
316
|
+
try {
|
|
317
|
+
meta = await this.inspectCandidate(parsed.spec, controller.signal);
|
|
318
|
+
} finally {
|
|
319
|
+
clearTimeout(timeout);
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
ok: meta.bundle,
|
|
323
|
+
spec: parsed.spec,
|
|
324
|
+
source: parsed.source,
|
|
325
|
+
inspection: meta,
|
|
326
|
+
message: meta.bundle ? "可安装的 DSH Bundle" : meta.reason || "不是 DSH Bundle 或 inspect 失败",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async listInstalled() {
|
|
331
|
+
if (!this.config.enabled) {
|
|
332
|
+
return { ok: false, message: "发现插件已停用", names: [], bundles: [] };
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const [bundles, plugins] = await Promise.all([
|
|
336
|
+
this.ctx.pluginManager.listBundles(),
|
|
337
|
+
this.ctx.pluginManager.listPlugins(),
|
|
338
|
+
]);
|
|
339
|
+
const rows = (Array.isArray(bundles) ? bundles : []).map((bundle) => ({
|
|
340
|
+
name: bundle.name,
|
|
341
|
+
version: bundle.version,
|
|
342
|
+
enabled: bundle.enabled === true,
|
|
343
|
+
installed: bundle.installed === true,
|
|
344
|
+
}));
|
|
345
|
+
const names = new Set(rows.map((row) => row.name).filter(Boolean));
|
|
346
|
+
for (const plugin of Array.isArray(plugins) ? plugins : []) {
|
|
347
|
+
const moduleName = plugin && plugin.moduleName;
|
|
348
|
+
if (!moduleName) continue;
|
|
349
|
+
names.add(moduleName);
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
ok: true,
|
|
353
|
+
names: [...names],
|
|
354
|
+
bundles: rows,
|
|
355
|
+
};
|
|
356
|
+
} catch (error) {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
message: error instanceof Error ? error.message : String(error),
|
|
360
|
+
names: [],
|
|
361
|
+
bundles: [],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async findExistingBundleName(candidates) {
|
|
367
|
+
const listed = await this.listInstalled();
|
|
368
|
+
if (!listed.ok) return { existing: null, listed };
|
|
369
|
+
const names = new Set(listed.names);
|
|
370
|
+
// 仅精确匹配包名/spec,避免 dsh-pet 与 @scope/dsh-pet 互相误判为已安装
|
|
371
|
+
for (const candidate of candidates) {
|
|
372
|
+
if (!candidate) continue;
|
|
373
|
+
if (names.has(candidate)) return { existing: candidate, listed };
|
|
374
|
+
}
|
|
375
|
+
return { existing: null, listed };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async installPlugin(spec, options) {
|
|
379
|
+
if (!this.config.enabled) {
|
|
380
|
+
return { ok: false, message: "发现插件已停用" };
|
|
381
|
+
}
|
|
382
|
+
const trimmed = String(spec || "").trim();
|
|
383
|
+
if (!trimmed) {
|
|
384
|
+
return { ok: false, message: "安装 spec 为空" };
|
|
385
|
+
}
|
|
386
|
+
const opts = options && typeof options === "object" ? options : {};
|
|
387
|
+
const requestId =
|
|
388
|
+
typeof opts.requestId === "string" && opts.requestId.trim()
|
|
389
|
+
? opts.requestId.trim()
|
|
390
|
+
: globalThis.crypto?.randomUUID?.() || `pd-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
391
|
+
const pre = await this.inspectCandidate(trimmed);
|
|
392
|
+
if (!pre.bundle) {
|
|
393
|
+
return { ok: false, requestId, message: pre.reason || "该 spec 不是可安装的 DSH Bundle" };
|
|
394
|
+
}
|
|
395
|
+
const { existing } = await this.findExistingBundleName([pre.name, trimmed]);
|
|
396
|
+
if (existing) {
|
|
397
|
+
return {
|
|
398
|
+
ok: false,
|
|
399
|
+
alreadyInstalled: true,
|
|
400
|
+
packageName: existing,
|
|
401
|
+
requestId,
|
|
402
|
+
message: `「${existing}」已在 DSH 插件列表中,无需重复安装。请到「插件」页查看。`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
const result = await this.ctx.pluginManager.installBundle(trimmed, {
|
|
407
|
+
requestId,
|
|
408
|
+
enabled: true,
|
|
409
|
+
});
|
|
410
|
+
const packageName = result?.bundle || pre.name || trimmed;
|
|
411
|
+
const application = result?.application;
|
|
412
|
+
const failed = application === "failed" || application === "cancelled";
|
|
413
|
+
return {
|
|
414
|
+
ok: !failed,
|
|
415
|
+
alreadyInstalled: false,
|
|
416
|
+
packageName,
|
|
417
|
+
requestId,
|
|
418
|
+
application,
|
|
419
|
+
stage: result?.stage,
|
|
420
|
+
message: failed
|
|
421
|
+
? result?.error?.message || `安装失败(${application || "unknown"})`
|
|
422
|
+
: `「${packageName}」安装完成(${application || "applied"} / ${result?.stage || "install"})`,
|
|
423
|
+
result,
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
ok: false,
|
|
428
|
+
requestId,
|
|
429
|
+
message: error instanceof Error ? error.message : String(error),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async waitInstall(requestId) {
|
|
435
|
+
const id = String(requestId || "").trim();
|
|
436
|
+
if (!id) return { ok: false, message: "requestId 为空", settled: null };
|
|
437
|
+
try {
|
|
438
|
+
const settled = await this.ctx.pluginManager.waitForInstall(id);
|
|
439
|
+
if (settled == null) {
|
|
440
|
+
return { ok: true, settled: null, pending: true, message: "安装仍在进行或已丢失" };
|
|
441
|
+
}
|
|
442
|
+
const failed = settled.application === "failed" || settled.application === "cancelled";
|
|
443
|
+
return {
|
|
444
|
+
ok: !failed,
|
|
445
|
+
settled,
|
|
446
|
+
pending: false,
|
|
447
|
+
packageName: settled.bundle,
|
|
448
|
+
application: settled.application,
|
|
449
|
+
stage: settled.stage,
|
|
450
|
+
message: failed
|
|
451
|
+
? settled.error?.message || `安装失败(${settled.application})`
|
|
452
|
+
: `安装完成(${settled.application} / ${settled.stage})`,
|
|
453
|
+
};
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
settled: null,
|
|
458
|
+
message: error instanceof Error ? error.message : String(error),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
registerTools() {
|
|
464
|
+
const service = this;
|
|
465
|
+
const disposers = [];
|
|
466
|
+
const present = (title) => ({ title, kind: "read" });
|
|
467
|
+
const toolOutput = {
|
|
468
|
+
schema: { type: "object", additionalProperties: true },
|
|
469
|
+
render: (_args, value) => [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
470
|
+
};
|
|
471
|
+
disposers.push(
|
|
472
|
+
this.ctx.tools.register(
|
|
473
|
+
defineTool({
|
|
474
|
+
name: "plugin_discovery_search",
|
|
475
|
+
description:
|
|
476
|
+
"Search GitHub and npm for community DSH plugin bundles (online only). Returns verified bundle install specs. Does not install.",
|
|
477
|
+
parameters: {
|
|
478
|
+
query: { type: "string", required: true, description: "Search keywords" },
|
|
479
|
+
source: {
|
|
480
|
+
type: "string",
|
|
481
|
+
enum: ["all", "npm", "github"],
|
|
482
|
+
description: "Limit search source",
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
output: toolOutput,
|
|
486
|
+
async execute(args) {
|
|
487
|
+
return service.search(args.query, { source: args.source || "all" });
|
|
488
|
+
},
|
|
489
|
+
presentCall: (_args, partial) =>
|
|
490
|
+
present(`Search DSH plugins: ${partial?.query ?? "…"}`),
|
|
491
|
+
}),
|
|
492
|
+
),
|
|
493
|
+
);
|
|
494
|
+
disposers.push(
|
|
495
|
+
this.ctx.tools.register(
|
|
496
|
+
defineTool({
|
|
497
|
+
name: "plugin_discovery_resolve",
|
|
498
|
+
description: "Resolve an npm or GitHub URL/package name and inspect whether it is a DSH bundle.",
|
|
499
|
+
parameters: {
|
|
500
|
+
input: {
|
|
501
|
+
type: "string",
|
|
502
|
+
required: true,
|
|
503
|
+
description: "npm URL, GitHub repo URL, or package name",
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
output: toolOutput,
|
|
507
|
+
async execute(args) {
|
|
508
|
+
return service.resolve(args.input);
|
|
509
|
+
},
|
|
510
|
+
presentCall: () => present("Resolve plugin URL"),
|
|
511
|
+
}),
|
|
512
|
+
),
|
|
513
|
+
);
|
|
514
|
+
return () => {
|
|
515
|
+
for (const dispose of disposers.reverse()) dispose();
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async [Service.init]() {}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "search");
|
|
523
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "resolve");
|
|
524
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "installPlugin");
|
|
525
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "listInstalled");
|
|
526
|
+
markRemoteMethod(PluginDiscoveryService.prototype, "waitInstall");
|
|
527
|
+
|
|
528
|
+
export default PluginDiscoveryService;
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Discover DSH plugins",
|
|
4
|
+
"description": "Search GitHub and npm for community DSH bundles; install after inspect."
|
|
5
|
+
},
|
|
6
|
+
"panel": "Discover",
|
|
7
|
+
"searchPlaceholder": "Keywords, e.g. pet, wallpaper, dsh",
|
|
8
|
+
"search": "Search",
|
|
9
|
+
"searching": "Searching…",
|
|
10
|
+
"sourceAll": "All",
|
|
11
|
+
"sourceNpm": "npm",
|
|
12
|
+
"sourceGithub": "GitHub",
|
|
13
|
+
"warning": "Community plugins are not official; review the repo before installing.",
|
|
14
|
+
"noResults": "No DSH bundles matched.",
|
|
15
|
+
"install": "Install",
|
|
16
|
+
"installing": "Installing…",
|
|
17
|
+
"inspectFailed": "Inspect failed",
|
|
18
|
+
"bundle": "Bundle",
|
|
19
|
+
"spec": "Install spec",
|
|
20
|
+
"resolvePlaceholder": "Paste npm or GitHub URL",
|
|
21
|
+
"resolve": "Resolve"
|
|
22
|
+
}
|
package/locale/zh.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "发现 DSH 插件",
|
|
4
|
+
"description": "在 GitHub 与 npm 上搜索社区 DSH Bundle,经 inspect 校验后安装。"
|
|
5
|
+
},
|
|
6
|
+
"panel": "发现插件",
|
|
7
|
+
"searchPlaceholder": "搜索关键词,例如 桌宠、壁纸、dsh",
|
|
8
|
+
"search": "搜索",
|
|
9
|
+
"searching": "搜索中…",
|
|
10
|
+
"sourceAll": "全部",
|
|
11
|
+
"sourceNpm": "npm",
|
|
12
|
+
"sourceGithub": "GitHub",
|
|
13
|
+
"warning": "社区插件非 DeepSeek 官方维护,安装前请自行审阅仓库与权限。",
|
|
14
|
+
"noResults": "未找到符合条件的 DSH Bundle。",
|
|
15
|
+
"install": "安装",
|
|
16
|
+
"installing": "安装中…",
|
|
17
|
+
"inspectFailed": "无法校验",
|
|
18
|
+
"bundle": "Bundle",
|
|
19
|
+
"spec": "安装 spec",
|
|
20
|
+
"resolvePlaceholder": "粘贴 npm 或 GitHub 链接",
|
|
21
|
+
"resolve": "解析并校验"
|
|
22
|
+
}
|
package/mark-remote.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Mark Typert Remote methods on a service prototype (plain JS, no TS decorators). */
|
|
2
|
+
const REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
|
|
3
|
+
|
|
4
|
+
function readRemoteMethodDescriptor(prototype) {
|
|
5
|
+
const property = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
|
|
6
|
+
if (property === undefined) return undefined;
|
|
7
|
+
const descriptor = property.value;
|
|
8
|
+
if (descriptor === null || typeof descriptor !== "object") {
|
|
9
|
+
throw new TypeError("typert-protocol: Remote method descriptor must be an object");
|
|
10
|
+
}
|
|
11
|
+
if (Reflect.get(descriptor, "version") !== 1) {
|
|
12
|
+
throw new TypeError("typert-protocol: unsupported Remote method descriptor version");
|
|
13
|
+
}
|
|
14
|
+
const methods = Reflect.get(descriptor, "methods");
|
|
15
|
+
if (!Array.isArray(methods)) {
|
|
16
|
+
throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
|
|
17
|
+
}
|
|
18
|
+
return descriptor;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function markRemoteMethod(prototype, method, exportName = method) {
|
|
22
|
+
const invocation = Object.freeze({ kind: "direct" });
|
|
23
|
+
const descriptor = readRemoteMethodDescriptor(prototype);
|
|
24
|
+
const marker = Object.freeze({
|
|
25
|
+
method,
|
|
26
|
+
...(exportName === method ? {} : { exportName }),
|
|
27
|
+
invocation,
|
|
28
|
+
});
|
|
29
|
+
const current = descriptor?.methods.find((candidate) => candidate.method === method);
|
|
30
|
+
if (current !== undefined) {
|
|
31
|
+
if (
|
|
32
|
+
current.exportName === marker.exportName &&
|
|
33
|
+
current.mode === marker.mode &&
|
|
34
|
+
current.invocation.kind === "direct" &&
|
|
35
|
+
invocation.kind === "direct"
|
|
36
|
+
) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
|
|
40
|
+
}
|
|
41
|
+
Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
|
|
42
|
+
configurable: true,
|
|
43
|
+
value: Object.freeze({
|
|
44
|
+
version: 1,
|
|
45
|
+
methods: Object.freeze([...(descriptor?.methods ?? []), marker]),
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-plugin-discovery",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-discovery",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Discover community DSH plugin bundles on GitHub/npm and install them from the DeepSeek Harness UI.",
|
|
6
|
+
"author": "hjhcos",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"dsh",
|
|
10
|
+
"deepseek",
|
|
11
|
+
"deepseek-harness",
|
|
12
|
+
"plugin",
|
|
13
|
+
"bundle",
|
|
14
|
+
"discovery"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/hjhcos/dsh-plugin-discovery#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/hjhcos/dsh-plugin-discovery/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/hjhcos/dsh-plugin-discovery.git"
|
|
23
|
+
},
|
|
24
|
+
"icon": "./icon.svg",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./index.js",
|
|
27
|
+
"./client": "./client.js",
|
|
28
|
+
"./package.json": "./package.json",
|
|
29
|
+
"./locale/*.json": "./locale/*.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"index.js",
|
|
33
|
+
"mark-remote.js",
|
|
34
|
+
"client.js",
|
|
35
|
+
"cordis.patch.yml",
|
|
36
|
+
"icon.svg",
|
|
37
|
+
"locale",
|
|
38
|
+
"skill",
|
|
39
|
+
"docs",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
45
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
|
|
47
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
51
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
|
|
53
|
+
"@deepseek-ai/schemastery": "*"
|
|
54
|
+
},
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./cordis.patch.yml"
|
|
58
|
+
},
|
|
59
|
+
"client": {
|
|
60
|
+
"platform": "web",
|
|
61
|
+
"immediately": false,
|
|
62
|
+
"inject": [
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale",
|
|
66
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|