dsh-m 0.1.0 → 0.2.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/README.en.md +104 -0
- package/README.md +61 -23
- package/docs/DESIGN.md +172 -0
- package/lib/cli.js +134 -63
- package/lib/client.js +1254 -217
- package/lib/core/dsh-cli.js +118 -10
- package/lib/core/host-api.js +285 -0
- package/lib/core/httpx.js +93 -36
- package/lib/core/installed.js +69 -4
- package/lib/core/market.js +481 -104
- package/lib/core/npm-integrity.js +141 -0
- package/lib/core/progress.js +113 -0
- package/lib/core/registry-check.js +111 -0
- package/lib/core/registry-controller.js +321 -0
- package/lib/core/registry.js +634 -98
- package/lib/core/versions.js +80 -9
- package/lib/host.js +22 -161
- package/lib/tools.js +56 -41
- package/package.json +4 -2
- package/registry.json +95 -7
- package/DESIGN.md +0 -140
package/lib/core/versions.js
CHANGED
|
@@ -1,14 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 最新版本解析(DESIGN.md §3:版本不写死,运行时实查)。
|
|
3
3
|
* npm:registry /latest(pnpm 安装本身会按 lock integrity 校验 tarball)。
|
|
4
|
-
* GitHub
|
|
4
|
+
* GitHub:优先最新 release/tag(更新提示只跟稳定版走,不跟 main HEAD——中间提交可能不稳定);
|
|
5
|
+
* 未认证限额 60 次/小时,自用足够。
|
|
5
6
|
*/
|
|
6
|
-
import { fetchJsonLimited } from './httpx.js';
|
|
7
|
-
|
|
7
|
+
import { fetchJsonLimited, HttpError } from './httpx.js';
|
|
8
|
+
/** GitHub 匿名限额(60 次/小时/IP)用尽时返回可读提示(含重置等待分钟数),否则 null。 */
|
|
9
|
+
function githubRateLimitMessage(err) {
|
|
10
|
+
if (err instanceof HttpError && err.status === 403 && err.headers?.get('x-ratelimit-remaining') === '0') {
|
|
11
|
+
const resetSec = Number(err.headers.get('x-ratelimit-reset'));
|
|
12
|
+
const waitMin = Number.isFinite(resetSec) && resetSec > 0
|
|
13
|
+
? Math.max(1, Math.ceil((resetSec * 1000 - Date.now()) / 60_000))
|
|
14
|
+
: null;
|
|
15
|
+
return waitMin
|
|
16
|
+
? `GitHub API 匿名限额已用尽(60 次/小时),约 ${waitMin} 分钟后自动重置`
|
|
17
|
+
: 'GitHub API 匿名限额已用尽(60 次/小时),请稍后重试';
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/** 精确 semver:接受 prerelease/build metadata,拒绝 v 前缀、range、tag 与脏尾缀。 */
|
|
22
|
+
const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
23
|
+
export function isExactVersion(version) {
|
|
24
|
+
return EXACT_VERSION_RE.test(version);
|
|
25
|
+
}
|
|
26
|
+
/** 读取该精确版本的 dist metadata(不使用 /latest endpoint);integrity 缺失由调用方拒绝安装。 */
|
|
27
|
+
export async function npmVersion(pkg, version, timeoutMs = 20_000, signal) {
|
|
28
|
+
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
29
|
+
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
30
|
+
if (!isExactVersion(version))
|
|
31
|
+
throw new Error(`不是精确版本(拒绝 range/tag/前缀): ${version}`);
|
|
32
|
+
const data = await fetchJsonLimited(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/${version}`, { timeoutMs, signal });
|
|
33
|
+
const resolved = typeof data.version === 'string' ? data.version : '';
|
|
34
|
+
if (!resolved)
|
|
35
|
+
throw new Error(`npm 未返回版本: ${pkg}@${version}`);
|
|
36
|
+
return {
|
|
37
|
+
version: resolved,
|
|
38
|
+
integrity: typeof data.dist?.integrity === 'string' ? data.dist.integrity : undefined,
|
|
39
|
+
tarball: typeof data.dist?.tarball === 'string' ? data.dist.tarball : undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function npmLatest(pkg, timeoutMs = 20_000, signal) {
|
|
8
43
|
// 允许 scoped 包名:@scope/name(isSafePkgName 同款字符集)
|
|
9
44
|
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
10
45
|
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
11
|
-
const data = await fetchJsonLimited(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`, { timeoutMs });
|
|
46
|
+
const data = await fetchJsonLimited(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`, { timeoutMs, signal });
|
|
12
47
|
const version = typeof data.version === 'string' ? data.version : '';
|
|
13
48
|
if (!version)
|
|
14
49
|
throw new Error(`npm 未返回版本: ${pkg}`);
|
|
@@ -18,18 +53,54 @@ export async function npmLatest(pkg, timeoutMs = 20_000) {
|
|
|
18
53
|
tarball: typeof data.dist?.tarball === 'string' ? data.dist.tarball : undefined,
|
|
19
54
|
};
|
|
20
55
|
}
|
|
21
|
-
export async function
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const data = await fetchJsonLimited(`https://api.github.com/repos/${repo}/commits/HEAD`, {
|
|
56
|
+
export async function githubTagSha(repo, tag, timeoutMs = 20_000, signal) {
|
|
57
|
+
// commits/{ref} 会自动解引用 annotated tag,返回的才是可用于 #sha 锁定的 commit
|
|
58
|
+
const data = await fetchJsonLimited(`https://api.github.com/repos/${repo}/commits/${encodeURIComponent(tag)}`, {
|
|
25
59
|
timeoutMs,
|
|
60
|
+
signal,
|
|
26
61
|
headers: { accept: 'application/vnd.github+json' },
|
|
27
62
|
});
|
|
28
63
|
const sha = typeof data.sha === 'string' ? data.sha : '';
|
|
29
64
|
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
30
|
-
throw new Error(`GitHub 未返回有效 SHA: ${repo}`);
|
|
65
|
+
throw new Error(`GitHub 未返回有效 SHA: ${repo}@${tag}`);
|
|
31
66
|
return sha;
|
|
32
67
|
}
|
|
68
|
+
/** GitHub 来源的“最新稳定点”:优先最新 release(排除 draft/prerelease),无则回退 tags 列表首项。 */
|
|
69
|
+
export async function githubLatestTag(repo, timeoutMs = 20_000, signal) {
|
|
70
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/.test(repo))
|
|
71
|
+
throw new Error(`无效 GitHub 仓库: ${repo}`);
|
|
72
|
+
const headers = { accept: 'application/vnd.github+json' };
|
|
73
|
+
try {
|
|
74
|
+
// 1) 最新 release(404 = 仓库从未发过 release → 回退 tags)
|
|
75
|
+
try {
|
|
76
|
+
const rel = await fetchJsonLimited(`https://api.github.com/repos/${repo}/releases/latest`, { timeoutMs, signal, headers });
|
|
77
|
+
const tag = typeof rel.tag_name === 'string' ? rel.tag_name.trim() : '';
|
|
78
|
+
if (tag)
|
|
79
|
+
return { tag, sha: await githubTagSha(repo, tag, timeoutMs, signal) };
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const status = err.status;
|
|
83
|
+
if (status !== 404)
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
// 2) 回退:tags 列表(GitHub 按创建时间倒序,首项即最新)
|
|
87
|
+
const tags = await fetchJsonLimited(`https://api.github.com/repos/${repo}/tags`, { timeoutMs, signal, headers });
|
|
88
|
+
if (!Array.isArray(tags) || !tags.length)
|
|
89
|
+
throw new Error(`仓库没有任何 tag: ${repo}`);
|
|
90
|
+
const first = tags[0];
|
|
91
|
+
const name = typeof first.name === 'string' ? first.name : '';
|
|
92
|
+
const sha = first.commit && typeof first.commit.sha === 'string' ? first.commit.sha : '';
|
|
93
|
+
if (!name || !/^[0-9a-f]{40}$/.test(sha))
|
|
94
|
+
throw new Error(`tag 信息无效: ${repo}`);
|
|
95
|
+
return { tag: name, sha };
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const rateLimit = githubRateLimitMessage(err);
|
|
99
|
+
if (rateLimit)
|
|
100
|
+
throw new Error(rateLimit);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
33
104
|
export function isNewerVersion(candidate, current) {
|
|
34
105
|
const parse = (v) => String(v || '')
|
|
35
106
|
.replace(/^v/i, '')
|
package/lib/host.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
-
import {
|
|
3
|
+
import { withMutationLock } from './core/market.js';
|
|
4
|
+
import { createApiDispatcher } from './core/host-api.js';
|
|
4
5
|
import { bindLoaderHost } from './core/live-plugin.js';
|
|
5
|
-
import {
|
|
6
|
+
import { createRegistryController } from './core/registry-controller.js';
|
|
6
7
|
import { registerTools } from './tools.js';
|
|
7
8
|
const require = createRequire(import.meta.url);
|
|
8
9
|
const pkg = require('../package.json');
|
|
@@ -10,178 +11,38 @@ export const name = 'dshm';
|
|
|
10
11
|
// dshm_* 七个工具(src/tools.ts)
|
|
11
12
|
export const inject = ['tools'];
|
|
12
13
|
export const Config = Schema.object({
|
|
13
|
-
registryUrl: Schema.string().description('registry
|
|
14
|
+
registryUrl: Schema.string().description('registry 地址:空值使用默认官方清单;支持 HTTPS URL、loopback HTTP URL 或本机绝对路径/file://(整体覆盖默认清单,live 生效)'),
|
|
14
15
|
timeoutMs: Schema.number().default(20000).description('上游请求超时(毫秒)'),
|
|
15
16
|
cacheTtlMin: Schema.number().default(60).description('registry 缓存时长(分钟)'),
|
|
16
17
|
});
|
|
17
18
|
export function apply(ctx, config) {
|
|
18
|
-
const cfg = { ...config };
|
|
19
19
|
// 卸载前的 live-disable 依赖 loader(skillhub 同款)
|
|
20
20
|
bindLoaderHost(ctx);
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// registry controller:active config / configured / pending / rejected 分离 + generation fence;
|
|
22
|
+
// tools 与 Host API 共用同一 active config object(apply 原地更新字段,live 生效)
|
|
23
|
+
const controller = createRegistryController(config);
|
|
24
|
+
registerTools(ctx, controller.config);
|
|
25
|
+
// 设置页(GUI 设置卡片的宿主命名空间):applies 'live',scope 提供 get/update/watch
|
|
26
|
+
ctx.inject(['settings'], (c) => {
|
|
27
|
+
const settings = c.settings;
|
|
28
|
+
const scope = settings.register('dshm', Config, { base: config, applies: 'live' });
|
|
29
|
+
const store = {
|
|
30
|
+
get: () => scope.get(),
|
|
31
|
+
update: (patch) => scope.update(patch),
|
|
32
|
+
watch: (callback) => scope.watch(callback),
|
|
33
|
+
};
|
|
34
|
+
controller.attachStore(store);
|
|
35
|
+
});
|
|
36
|
+
// 本地 API:单路由 + method 分发(防护与状态映射在 core/host-api.ts)
|
|
24
37
|
ctx.inject(['webServer'], (c) => {
|
|
25
38
|
const server = c.webServer;
|
|
39
|
+
const handleApi = createApiDispatcher({ controller, pkg, onMutation: withMutationLock });
|
|
26
40
|
server.register({
|
|
27
41
|
kind: 'exact',
|
|
28
42
|
path: '/dshm',
|
|
29
43
|
handler: (req, res) => {
|
|
30
|
-
void handleApi(req, res
|
|
44
|
+
void handleApi(req, res);
|
|
31
45
|
},
|
|
32
46
|
});
|
|
33
47
|
});
|
|
34
|
-
// 设置页(GUI 设置卡片的宿主命名空间)
|
|
35
|
-
ctx.inject(['settings'], (c) => {
|
|
36
|
-
const settings = c.settings;
|
|
37
|
-
settings.register('dshm', Config, { base: config });
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
async function handleApi(req, res, cfg) {
|
|
41
|
-
try {
|
|
42
|
-
const url = new URL(req.url || '/', 'http://127.0.0.1');
|
|
43
|
-
const body = req.method === 'POST' ? await readBody(req) : {};
|
|
44
|
-
const method = String(body.method || url.searchParams.get('method') || 'ping');
|
|
45
|
-
switch (method) {
|
|
46
|
-
case 'ping':
|
|
47
|
-
return sendJson(res, 200, {
|
|
48
|
-
ok: true,
|
|
49
|
-
plugin: pkg.name,
|
|
50
|
-
version: pkg.version,
|
|
51
|
-
node: process.version,
|
|
52
|
-
boot: `${process.pid}`,
|
|
53
|
-
});
|
|
54
|
-
case 'self-check': {
|
|
55
|
-
try {
|
|
56
|
-
const { npmLatest } = await import('./core/versions.js');
|
|
57
|
-
const latest = await npmLatest(pkg.name, cfg.timeoutMs ?? 20_000);
|
|
58
|
-
const { isNewerVersion } = await import('./core/versions.js');
|
|
59
|
-
return sendJson(res, 200, {
|
|
60
|
-
ok: true,
|
|
61
|
-
current: pkg.version,
|
|
62
|
-
latest: latest.version,
|
|
63
|
-
outdated: isNewerVersion(latest.version, pkg.version),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
return sendJson(res, 200, {
|
|
68
|
-
ok: true,
|
|
69
|
-
current: pkg.version,
|
|
70
|
-
latest: null,
|
|
71
|
-
outdated: false,
|
|
72
|
-
error: err instanceof Error ? err.message : String(err),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
case 'self-upgrade': {
|
|
77
|
-
const { npmLatest } = await import('./core/versions.js');
|
|
78
|
-
const latest = await npmLatest(pkg.name, cfg.timeoutMs ?? 20_000);
|
|
79
|
-
const result = await withMutationLock(async () => {
|
|
80
|
-
const { addDshPlugin } = await import('./core/dsh-cli.js');
|
|
81
|
-
return addDshPlugin(`${pkg.name}@${latest.version}`);
|
|
82
|
-
});
|
|
83
|
-
return sendJson(res, 200, {
|
|
84
|
-
ok: true,
|
|
85
|
-
pkg: pkg.name,
|
|
86
|
-
version: latest.version,
|
|
87
|
-
usedAllowAllBuilds: result.usedAllowAllBuilds,
|
|
88
|
-
needsRestart: true,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
case 'registry': {
|
|
92
|
-
const loaded = await loadRegistrySafe(cfg, boolArg(body.force));
|
|
93
|
-
return sendJson(res, 200, {
|
|
94
|
-
ok: true,
|
|
95
|
-
plugins: loaded.registry.plugins,
|
|
96
|
-
source: loaded.source,
|
|
97
|
-
fetchedAt: loaded.fetchedAt,
|
|
98
|
-
errors: loaded.errors,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
case 'market': {
|
|
102
|
-
const result = await listMarket(cfg, { force: boolArg(body.force) });
|
|
103
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
104
|
-
}
|
|
105
|
-
case 'installed': {
|
|
106
|
-
const result = await listInstalledWithMeta(cfg);
|
|
107
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
108
|
-
}
|
|
109
|
-
case 'install': {
|
|
110
|
-
const id = String(body.id || '').trim();
|
|
111
|
-
if (!id)
|
|
112
|
-
return sendJson(res, 400, { ok: false, error: '缺少 id' });
|
|
113
|
-
const version = typeof body.version === 'string' ? body.version : undefined;
|
|
114
|
-
const result = await withMutationLock(() => installFromRegistry(id, cfg, { version }));
|
|
115
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
116
|
-
}
|
|
117
|
-
case 'uninstall': {
|
|
118
|
-
const target = String(body.pkg || '').trim();
|
|
119
|
-
if (!target)
|
|
120
|
-
return sendJson(res, 400, { ok: false, error: '缺少 pkg' });
|
|
121
|
-
const result = await withMutationLock(() => uninstallPlugin(target, cfg));
|
|
122
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
123
|
-
}
|
|
124
|
-
case 'upgrade': {
|
|
125
|
-
const target = String(body.pkg || '').trim();
|
|
126
|
-
if (!target)
|
|
127
|
-
return sendJson(res, 400, { ok: false, error: '缺少 pkg' });
|
|
128
|
-
const result = await withMutationLock(() => upgradePlugin(target, cfg));
|
|
129
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
130
|
-
}
|
|
131
|
-
case 'restart': {
|
|
132
|
-
if (!trustedRestartRequest(req)) {
|
|
133
|
-
return sendJson(res, 403, { ok: false, error: '拒绝跨源重启请求' });
|
|
134
|
-
}
|
|
135
|
-
const result = scheduleRestart(servingPort(req));
|
|
136
|
-
return sendJson(res, 200, { ok: true, ...result });
|
|
137
|
-
}
|
|
138
|
-
default:
|
|
139
|
-
return sendJson(res, 404, { ok: false, error: `未知 method: ${method}` });
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
143
|
-
return sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async function loadRegistrySafe(cfg, force) {
|
|
147
|
-
const { loadRegistry } = await import('./core/registry.js');
|
|
148
|
-
return loadRegistry(cfg, { force });
|
|
149
|
-
}
|
|
150
|
-
function boolArg(v) {
|
|
151
|
-
return v === true || v === 'true' || v === 1 || v === '1';
|
|
152
|
-
}
|
|
153
|
-
function readBody(req, maxBytes = 1 << 20) {
|
|
154
|
-
return new Promise((resolve, reject) => {
|
|
155
|
-
const chunks = [];
|
|
156
|
-
let size = 0;
|
|
157
|
-
req.on('data', (c) => {
|
|
158
|
-
size += c.length;
|
|
159
|
-
if (size > maxBytes) {
|
|
160
|
-
reject(new Error('请求体过大'));
|
|
161
|
-
req.destroy();
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
chunks.push(c);
|
|
165
|
-
});
|
|
166
|
-
req.on('end', () => {
|
|
167
|
-
if (!chunks.length)
|
|
168
|
-
return resolve({});
|
|
169
|
-
try {
|
|
170
|
-
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
resolve({});
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
req.on('error', reject);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
function sendJson(res, status, value) {
|
|
180
|
-
const body = JSON.stringify(value);
|
|
181
|
-
res.writeHead(status, {
|
|
182
|
-
'content-type': 'application/json; charset=utf-8',
|
|
183
|
-
'content-length': Buffer.byteLength(body),
|
|
184
|
-
'cache-control': 'no-store',
|
|
185
|
-
});
|
|
186
|
-
res.end(body);
|
|
187
48
|
}
|
package/lib/tools.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
2
|
import { installTimeoutMs } from './core/env.js';
|
|
3
|
-
import { installFromRegistry, listInstalledWithMeta, uninstallPlugin, upgradePlugin, withMutationLock } from './core/market.js';
|
|
3
|
+
import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, withMutationLock, } from './core/market.js';
|
|
4
4
|
import { scheduleRestart } from './core/restart.js';
|
|
5
5
|
export const CATEGORY_LABELS = {
|
|
6
6
|
market: '市场',
|
|
@@ -13,6 +13,9 @@ export const CATEGORY_LABELS = {
|
|
|
13
13
|
function cloneJson(value) {
|
|
14
14
|
return JSON.parse(JSON.stringify(value));
|
|
15
15
|
}
|
|
16
|
+
function summaryOf(state) {
|
|
17
|
+
return { isDefault: state.isDefault, status: state.status, stale: state.stale };
|
|
18
|
+
}
|
|
16
19
|
function matchInstalledByEntry(entry, installed) {
|
|
17
20
|
return installed.find((it) => {
|
|
18
21
|
if (entry.npm && (it.pkg === entry.npm || it.name === entry.npm))
|
|
@@ -25,8 +28,15 @@ function matchInstalledByEntry(entry, installed) {
|
|
|
25
28
|
return false;
|
|
26
29
|
});
|
|
27
30
|
}
|
|
28
|
-
export function registerTools(ctx, cfg) {
|
|
31
|
+
export function registerTools(ctx, cfg, deps = {}) {
|
|
29
32
|
const timeoutMs = cfg.timeoutMs ?? 20_000;
|
|
33
|
+
const m = {
|
|
34
|
+
listMarket: deps.listMarket ?? listMarket,
|
|
35
|
+
listInstalledWithMeta: deps.listInstalledWithMeta ?? listInstalledWithMeta,
|
|
36
|
+
installFromRegistry: deps.installFromRegistry ?? installFromRegistry,
|
|
37
|
+
uninstallPlugin: deps.uninstallPlugin ?? uninstallPlugin,
|
|
38
|
+
upgradePlugin: deps.upgradePlugin ?? upgradePlugin,
|
|
39
|
+
};
|
|
30
40
|
ctx.tools.register(defineTool({
|
|
31
41
|
name: 'dshm_search',
|
|
32
42
|
description: 'Search your personal DSH plugin marketplace (dsh-m) and show clickable plugin cards. ALWAYS call this instead of web_search or bash when the user wants to find/recommend/browse their curated DSH plugins (插件). Call EXACTLY ONCE per user message; extract a real keyword (主题, 搜索) rather than pasting the whole sentence. Omit query to browse all listings. After cards appear, reply with AT MOST one short sentence. Do not print install commands.',
|
|
@@ -56,44 +66,43 @@ export function registerTools(ctx, cfg) {
|
|
|
56
66
|
}),
|
|
57
67
|
timeoutMs: timeoutMs + 5000,
|
|
58
68
|
async execute(args) {
|
|
59
|
-
const
|
|
69
|
+
const category = typeof args.category === 'string' && args.category ? args.category : null;
|
|
70
|
+
const rawLimit = Number(args.limit);
|
|
71
|
+
const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? clamp(rawLimit, 1, 80) : undefined;
|
|
72
|
+
// metadata-only:agent 卡片不需要 latest;与 Host GUI 共用 host namespace
|
|
73
|
+
const result = await m.listMarket(cfg, {
|
|
74
|
+
query: String(args.query || ''),
|
|
75
|
+
category,
|
|
76
|
+
offset: 0,
|
|
77
|
+
limit,
|
|
78
|
+
withLatest: false,
|
|
79
|
+
namespace: 'host',
|
|
80
|
+
});
|
|
60
81
|
const installed = await import('./core/installed.js');
|
|
61
|
-
const loaded = await registry.loadRegistry(cfg);
|
|
62
82
|
const inst = await installed.listInstalledPlugins();
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (category && e.category !== category)
|
|
67
|
-
return false;
|
|
68
|
-
if (!query)
|
|
69
|
-
return true;
|
|
70
|
-
const hay = `${e.id} ${e.name} ${e.description} ${e.tags.join(' ')}`.toLowerCase();
|
|
71
|
-
return hay.includes(query);
|
|
83
|
+
const merged = result.items.map((e) => {
|
|
84
|
+
const i = matchInstalledByEntry(e, inst.items);
|
|
85
|
+
return { ...e, installed: Boolean(i), installedPkg: i?.pkg, installedVersion: i?.version };
|
|
72
86
|
});
|
|
73
|
-
const total = items.length;
|
|
74
|
-
const limit = Number.isFinite(Number(args.limit)) && Number(args.limit) > 0 ? clamp(Number(args.limit), 1, 80) : total;
|
|
75
|
-
items = items.slice(0, limit);
|
|
76
87
|
return cloneJson({
|
|
77
88
|
query: String(args.query || ''),
|
|
78
89
|
category,
|
|
79
|
-
total,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
96
|
-
}),
|
|
90
|
+
total: result.total,
|
|
91
|
+
registry: summaryOf(result.registryState),
|
|
92
|
+
items: merged.map((e) => ({
|
|
93
|
+
id: e.id,
|
|
94
|
+
name: e.name,
|
|
95
|
+
description: e.description,
|
|
96
|
+
category: e.category,
|
|
97
|
+
tags: e.tags,
|
|
98
|
+
source: e.source,
|
|
99
|
+
npm: e.npm,
|
|
100
|
+
github: e.github,
|
|
101
|
+
homepage: e.homepage,
|
|
102
|
+
installed: e.installed,
|
|
103
|
+
installedPkg: e.installedPkg,
|
|
104
|
+
installedVersion: e.installedVersion,
|
|
105
|
+
})),
|
|
97
106
|
});
|
|
98
107
|
},
|
|
99
108
|
}));
|
|
@@ -114,8 +123,9 @@ export function registerTools(ctx, cfg) {
|
|
|
114
123
|
}),
|
|
115
124
|
timeoutMs: timeoutMs + 5000,
|
|
116
125
|
async execute() {
|
|
117
|
-
const result = await listInstalledWithMeta(cfg);
|
|
126
|
+
const result = await m.listInstalledWithMeta(cfg, { namespace: 'host' });
|
|
118
127
|
return cloneJson({
|
|
128
|
+
registry: summaryOf(result.registryState),
|
|
119
129
|
profileDir: result.profileDir,
|
|
120
130
|
others: result.others,
|
|
121
131
|
items: result.items.map((it) => ({
|
|
@@ -154,7 +164,7 @@ export function registerTools(ctx, cfg) {
|
|
|
154
164
|
if (!id)
|
|
155
165
|
throw new Error('缺少收录 id');
|
|
156
166
|
const version = typeof args.version === 'string' && args.version.trim() ? args.version.trim() : undefined;
|
|
157
|
-
return cloneJson(await withMutationLock(() => installFromRegistry(id, cfg, { version })));
|
|
167
|
+
return cloneJson(await withMutationLock(() => m.installFromRegistry(id, cfg, { version, namespace: 'host' })));
|
|
158
168
|
},
|
|
159
169
|
}));
|
|
160
170
|
ctx.tools.register(defineTool({
|
|
@@ -179,7 +189,7 @@ export function registerTools(ctx, cfg) {
|
|
|
179
189
|
const target = String(args.pkg || '').trim();
|
|
180
190
|
if (!target)
|
|
181
191
|
throw new Error('缺少 pkg');
|
|
182
|
-
return cloneJson(await withMutationLock(() => uninstallPlugin(target, cfg)));
|
|
192
|
+
return cloneJson(await withMutationLock(() => m.uninstallPlugin(target, cfg, { namespace: 'host' })));
|
|
183
193
|
},
|
|
184
194
|
}));
|
|
185
195
|
ctx.tools.register(defineTool({
|
|
@@ -199,16 +209,21 @@ export function registerTools(ctx, cfg) {
|
|
|
199
209
|
},
|
|
200
210
|
timeoutMs: timeoutMs + 10_000,
|
|
201
211
|
async execute() {
|
|
202
|
-
const result = await listInstalledWithMeta(cfg);
|
|
212
|
+
const result = await m.listInstalledWithMeta(cfg, { namespace: 'host' });
|
|
203
213
|
const items = result.items.map((it) => ({
|
|
204
214
|
pkg: it.pkg,
|
|
205
215
|
name: it.name,
|
|
206
216
|
version: it.version,
|
|
207
217
|
source: it.source,
|
|
208
218
|
latestVersion: it.latestVersion ?? null,
|
|
219
|
+
latestTag: it.latestTag ?? null,
|
|
209
220
|
outdated: it.outdated,
|
|
210
221
|
}));
|
|
211
|
-
return cloneJson({
|
|
222
|
+
return cloneJson({
|
|
223
|
+
registry: summaryOf(result.registryState),
|
|
224
|
+
items,
|
|
225
|
+
outdatedCount: items.filter((it) => it.outdated).length,
|
|
226
|
+
});
|
|
212
227
|
},
|
|
213
228
|
}));
|
|
214
229
|
ctx.tools.register(defineTool({
|
|
@@ -233,7 +248,7 @@ export function registerTools(ctx, cfg) {
|
|
|
233
248
|
const target = String(args.pkg || '').trim();
|
|
234
249
|
if (!target)
|
|
235
250
|
throw new Error('缺少 pkg');
|
|
236
|
-
return cloneJson(await withMutationLock(() => upgradePlugin(target, cfg)));
|
|
251
|
+
return cloneJson(await withMutationLock(() => m.upgradePlugin(target, cfg, { namespace: 'host' })));
|
|
237
252
|
},
|
|
238
253
|
}));
|
|
239
254
|
ctx.tools.register(defineTool({
|
|
@@ -322,7 +337,7 @@ function renderOutdated(out) {
|
|
|
322
337
|
return 'web profile 没有已装插件。';
|
|
323
338
|
if (!outdated.length)
|
|
324
339
|
return `全部 ${out.items.length} 个插件均已是最新版本。对用户一句短话。`;
|
|
325
|
-
const lines = outdated.map((it) => `${it.name} (${it.pkg}):v${it.version} → ${it.
|
|
340
|
+
const lines = outdated.map((it) => `${it.name} (${it.pkg}):v${it.version} → ${it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : '最新')}`);
|
|
326
341
|
return [
|
|
327
342
|
`${outdated.length}/${out.items.length} 个插件可升级:`,
|
|
328
343
|
lines.join('\n'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-m",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "DSH Marketplace — 个人自用的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"cordis.patch.yml",
|
|
20
20
|
"registry.json",
|
|
21
21
|
"README.md",
|
|
22
|
-
"
|
|
22
|
+
"README.en.md",
|
|
23
|
+
"docs/DESIGN.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|
|
25
26
|
"keywords": [
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"scripts": {
|
|
44
45
|
"build": "node scripts/build.mjs",
|
|
45
46
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
+
"test": "node --test tests/*.test.mjs",
|
|
46
48
|
"prepare": "npm run build"
|
|
47
49
|
},
|
|
48
50
|
"dsh": {
|
package/registry.json
CHANGED
|
@@ -12,15 +12,103 @@
|
|
|
12
12
|
"homepage": "https://github.com/iasiv5/skins"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
|
-
"id": "
|
|
16
|
-
"name": "
|
|
17
|
-
"description": "
|
|
15
|
+
"id": "modsearch",
|
|
16
|
+
"name": "ModSearch",
|
|
17
|
+
"description": "免费网页搜索、X 搜索与页面抓取插件,支持 Firecrawl、Tavily、Exa 等引擎自动故障切换。",
|
|
18
18
|
"category": "search",
|
|
19
|
-
"tags": ["搜索", "
|
|
19
|
+
"tags": ["搜索", "网页抓取", "X"],
|
|
20
20
|
"source": "npm",
|
|
21
|
-
"npm": "
|
|
22
|
-
"github": "
|
|
23
|
-
"homepage": "https://github.com/
|
|
21
|
+
"npm": "@liustack/modsearch",
|
|
22
|
+
"github": "liustack/modsearch",
|
|
23
|
+
"homepage": "https://github.com/liustack/modsearch"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "deepseek-harness-lark",
|
|
27
|
+
"name": "DeepSeek Harness Lark",
|
|
28
|
+
"description": "飞书 / Lark 文本与图片通道插件:把飞书消息桥接进 DeepSeek Harness 对话。",
|
|
29
|
+
"category": "other",
|
|
30
|
+
"tags": ["飞书", "Lark", "通道"],
|
|
31
|
+
"source": "npm",
|
|
32
|
+
"npm": "deepseek-harness-lark",
|
|
33
|
+
"github": "sliverp/DeepSeek-harness-lark",
|
|
34
|
+
"homepage": "https://github.com/sliverp/DeepSeek-harness-lark"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "deepseek-harness-qqbot",
|
|
38
|
+
"name": "DeepSeek Harness QQBot",
|
|
39
|
+
"description": "QQ Bot 文本与图片通道插件:把 QQ 机器人消息桥接进 DeepSeek Harness 对话。",
|
|
40
|
+
"category": "other",
|
|
41
|
+
"tags": ["QQ", "通道"],
|
|
42
|
+
"source": "npm",
|
|
43
|
+
"npm": "deepseek-harness-qqbot",
|
|
44
|
+
"github": "sliverp/DeepSeek-harness-qqbot",
|
|
45
|
+
"homepage": "https://github.com/sliverp/DeepSeek-harness-qqbot"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "deepseek-harness-weixin",
|
|
49
|
+
"name": "DeepSeek Harness Weixin",
|
|
50
|
+
"description": "微信通道插件:扫码登录,收发文本与图片消息,桥接进 DeepSeek Harness 对话。",
|
|
51
|
+
"category": "other",
|
|
52
|
+
"tags": ["微信", "通道"],
|
|
53
|
+
"source": "npm",
|
|
54
|
+
"npm": "deepseek-harness-weixin",
|
|
55
|
+
"github": "sliverp/DeepSeek-harness-weixin",
|
|
56
|
+
"homepage": "https://github.com/sliverp/DeepSeek-harness-weixin"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "deepseek-harness-wecom",
|
|
60
|
+
"name": "DeepSeek Harness WeCom",
|
|
61
|
+
"description": "企业微信 AI Bot 文本与图片通道桥,桥接进 DeepSeek Harness 对话。",
|
|
62
|
+
"category": "other",
|
|
63
|
+
"tags": ["企业微信", "通道"],
|
|
64
|
+
"source": "npm",
|
|
65
|
+
"npm": "deepseek-harness-wecom",
|
|
66
|
+
"github": "sliverp/DeepSeek-harness-wecom",
|
|
67
|
+
"homepage": "https://github.com/sliverp/DeepSeek-harness-wecom"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "deepseek-harness-dingtalk",
|
|
71
|
+
"name": "DeepSeek Harness DingTalk",
|
|
72
|
+
"description": "钉钉 Stream 文本与图片通道插件,桥接进 DeepSeek Harness 对话。",
|
|
73
|
+
"category": "other",
|
|
74
|
+
"tags": ["钉钉", "通道"],
|
|
75
|
+
"source": "npm",
|
|
76
|
+
"npm": "deepseek-harness-dingtalk",
|
|
77
|
+
"github": "sliverp/DeepSeek-harness-dingtalk",
|
|
78
|
+
"homepage": "https://github.com/sliverp/DeepSeek-harness-dingtalk"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"id": "dsh-auth",
|
|
82
|
+
"name": "DSH Auth",
|
|
83
|
+
"description": "DSH Web 管理员认证插件:Argon2id 口令、可撤销会话、双语界面,经 Caddy forward_auth 边缘保护页面/API/下载/SSE/WebSocket,Harness 保持仅回环监听。",
|
|
84
|
+
"category": "tools",
|
|
85
|
+
"tags": ["认证", "安全"],
|
|
86
|
+
"source": "npm",
|
|
87
|
+
"npm": "dsh-auth",
|
|
88
|
+
"github": "hxy91819/dsh-auth",
|
|
89
|
+
"homepage": "https://github.com/hxy91819/dsh-auth"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "dsh-copilot-auth",
|
|
93
|
+
"name": "DSH Copilot Auth",
|
|
94
|
+
"description": "为 DSH 内置 GitHub Copilot 提供方补上设备码(device flow)登录/注销设置页,并预置开箱即用的 Copilot 路由;凭据托管 DSH 内置凭据库,模型目录自动同步,中英双语。",
|
|
95
|
+
"category": "tools",
|
|
96
|
+
"tags": ["Copilot", "认证", "模型接入"],
|
|
97
|
+
"source": "npm",
|
|
98
|
+
"npm": "@inventec/dsh-copilot-auth",
|
|
99
|
+
"github": "iasiv5/dsh-copilot-auth",
|
|
100
|
+
"homepage": "https://github.com/iasiv5/dsh-copilot-auth"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"id": "dsh-m",
|
|
104
|
+
"name": "DSH Marketplace",
|
|
105
|
+
"description": "个人自用的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件,含 Web 界面、agent 工具与 CLI。",
|
|
106
|
+
"category": "market",
|
|
107
|
+
"tags": ["市场", "插件管理"],
|
|
108
|
+
"source": "npm",
|
|
109
|
+
"npm": "dsh-m",
|
|
110
|
+
"github": "iasiv5/dsh-m",
|
|
111
|
+
"homepage": "https://github.com/iasiv5/dsh-m"
|
|
24
112
|
}
|
|
25
113
|
]
|
|
26
114
|
}
|