dsh-m 0.0.3 → 0.1.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/DESIGN.md +140 -0
- package/README.en.md +89 -0
- package/README.md +78 -11
- package/cordis.patch.yml +3 -0
- package/lib/.keep +0 -0
- package/lib/cli.js +196 -0
- package/lib/client.js +1159 -0
- package/lib/core/dsh-cli.js +320 -0
- package/lib/core/env.js +18 -0
- package/lib/core/httpx.js +94 -0
- package/lib/core/installed.js +186 -0
- package/lib/core/live-plugin.js +42 -0
- package/lib/core/market.js +198 -0
- package/lib/core/progress.js +113 -0
- package/lib/core/registry.js +167 -0
- package/lib/core/restart.js +213 -0
- package/lib/core/versions.js +75 -0
- package/lib/host.js +199 -0
- package/lib/tools.js +341 -0
- package/package.json +58 -7
- package/registry.json +92 -0
- package/index.js +0 -9
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 最新版本解析(DESIGN.md §3:版本不写死,运行时实查)。
|
|
3
|
+
* npm:registry /latest(pnpm 安装本身会按 lock integrity 校验 tarball)。
|
|
4
|
+
* GitHub:优先最新 release/tag(更新提示只跟稳定版走,不跟 main HEAD——中间提交可能不稳定);
|
|
5
|
+
* 未认证限额 60 次/小时,自用足够。
|
|
6
|
+
*/
|
|
7
|
+
import { fetchJsonLimited } from './httpx.js';
|
|
8
|
+
export async function npmLatest(pkg, timeoutMs = 20_000) {
|
|
9
|
+
// 允许 scoped 包名:@scope/name(isSafePkgName 同款字符集)
|
|
10
|
+
if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
|
|
11
|
+
throw new Error(`无效 npm 包名: ${pkg}`);
|
|
12
|
+
const data = await fetchJsonLimited(`https://registry.npmjs.org/${encodeURIComponent(pkg)}/latest`, { timeoutMs });
|
|
13
|
+
const version = typeof data.version === 'string' ? data.version : '';
|
|
14
|
+
if (!version)
|
|
15
|
+
throw new Error(`npm 未返回版本: ${pkg}`);
|
|
16
|
+
return {
|
|
17
|
+
version,
|
|
18
|
+
integrity: typeof data.dist?.integrity === 'string' ? data.dist.integrity : undefined,
|
|
19
|
+
tarball: typeof data.dist?.tarball === 'string' ? data.dist.tarball : undefined,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function githubTagSha(repo, tag, timeoutMs = 20_000) {
|
|
23
|
+
// commits/{ref} 会自动解引用 annotated tag,返回的才是可用于 #sha 锁定的 commit
|
|
24
|
+
const data = await fetchJsonLimited(`https://api.github.com/repos/${repo}/commits/${encodeURIComponent(tag)}`, {
|
|
25
|
+
timeoutMs,
|
|
26
|
+
headers: { accept: 'application/vnd.github+json' },
|
|
27
|
+
});
|
|
28
|
+
const sha = typeof data.sha === 'string' ? data.sha : '';
|
|
29
|
+
if (!/^[0-9a-f]{40}$/.test(sha))
|
|
30
|
+
throw new Error(`GitHub 未返回有效 SHA: ${repo}@${tag}`);
|
|
31
|
+
return sha;
|
|
32
|
+
}
|
|
33
|
+
/** GitHub 来源的“最新稳定点”:优先最新 release(排除 draft/prerelease),无则回退 tags 列表首项。 */
|
|
34
|
+
export async function githubLatestTag(repo, timeoutMs = 20_000) {
|
|
35
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9._-]+$/.test(repo))
|
|
36
|
+
throw new Error(`无效 GitHub 仓库: ${repo}`);
|
|
37
|
+
const headers = { accept: 'application/vnd.github+json' };
|
|
38
|
+
// 1) 最新 release(404 = 仓库从未发过 release → 回退 tags)
|
|
39
|
+
try {
|
|
40
|
+
const rel = await fetchJsonLimited(`https://api.github.com/repos/${repo}/releases/latest`, { timeoutMs, headers });
|
|
41
|
+
const tag = typeof rel.tag_name === 'string' ? rel.tag_name.trim() : '';
|
|
42
|
+
if (tag)
|
|
43
|
+
return { tag, sha: await githubTagSha(repo, tag, timeoutMs) };
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const status = err.status;
|
|
47
|
+
if (status !== 404)
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
// 2) 回退:tags 列表(GitHub 按创建时间倒序,首项即最新)
|
|
51
|
+
const tags = await fetchJsonLimited(`https://api.github.com/repos/${repo}/tags`, { timeoutMs, headers });
|
|
52
|
+
if (!Array.isArray(tags) || !tags.length)
|
|
53
|
+
throw new Error(`仓库没有任何 tag: ${repo}`);
|
|
54
|
+
const first = tags[0];
|
|
55
|
+
const name = typeof first.name === 'string' ? first.name : '';
|
|
56
|
+
const sha = first.commit && typeof first.commit.sha === 'string' ? first.commit.sha : '';
|
|
57
|
+
if (!name || !/^[0-9a-f]{40}$/.test(sha))
|
|
58
|
+
throw new Error(`tag 信息无效: ${repo}`);
|
|
59
|
+
return { tag: name, sha };
|
|
60
|
+
}
|
|
61
|
+
export function isNewerVersion(candidate, current) {
|
|
62
|
+
const parse = (v) => String(v || '')
|
|
63
|
+
.replace(/^v/i, '')
|
|
64
|
+
.split(/[-+.]/)
|
|
65
|
+
.map((part) => (/^\d+$/.test(part) ? Number(part) : part))
|
|
66
|
+
.map((n) => (typeof n === 'number' ? n : 0));
|
|
67
|
+
const a = parse(candidate);
|
|
68
|
+
const b = parse(current);
|
|
69
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
70
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0);
|
|
71
|
+
if (diff !== 0)
|
|
72
|
+
return diff > 0;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
package/lib/host.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
3
|
+
import { installFromRegistry, listInstalledWithMeta, listMarket, uninstallPlugin, upgradePlugin, withMutationLock, } from './core/market.js';
|
|
4
|
+
import { bindLoaderHost } from './core/live-plugin.js';
|
|
5
|
+
import { scheduleRestart, servingPort, trustedRestartRequest } from './core/restart.js';
|
|
6
|
+
import { registerTools } from './tools.js';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const pkg = require('../package.json');
|
|
9
|
+
export const name = 'dshm';
|
|
10
|
+
// dshm_* 七个工具(src/tools.ts)
|
|
11
|
+
export const inject = ['tools'];
|
|
12
|
+
export const Config = Schema.object({
|
|
13
|
+
registryUrl: Schema.string().description('registry 源覆盖(默认 jsDelivr @main)'),
|
|
14
|
+
timeoutMs: Schema.number().default(20000).description('上游请求超时(毫秒)'),
|
|
15
|
+
cacheTtlMin: Schema.number().default(60).description('registry 缓存时长(分钟)'),
|
|
16
|
+
});
|
|
17
|
+
export function apply(ctx, config) {
|
|
18
|
+
const cfg = { ...config };
|
|
19
|
+
// 卸载前的 live-disable 依赖 loader(skillhub 同款)
|
|
20
|
+
bindLoaderHost(ctx);
|
|
21
|
+
// dshm_* 七个 agent 工具 + systemPrompt 注入
|
|
22
|
+
registerTools(ctx, cfg);
|
|
23
|
+
// 本地 API:单路由 + method 分发(skillhub 同款)
|
|
24
|
+
ctx.inject(['webServer'], (c) => {
|
|
25
|
+
const server = c.webServer;
|
|
26
|
+
server.register({
|
|
27
|
+
kind: 'exact',
|
|
28
|
+
path: '/dshm',
|
|
29
|
+
handler: (req, res) => {
|
|
30
|
+
void handleApi(req, res, cfg);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
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 'readme': {
|
|
110
|
+
const target = String(body.pkg || '').trim();
|
|
111
|
+
if (!target)
|
|
112
|
+
return sendJson(res, 400, { ok: false, error: '缺少 pkg' });
|
|
113
|
+
const { readInstalledPluginReadme } = await import('./core/installed.js');
|
|
114
|
+
const result = await readInstalledPluginReadme(target);
|
|
115
|
+
return sendJson(res, 200, { ok: true, ...result });
|
|
116
|
+
}
|
|
117
|
+
case 'status': {
|
|
118
|
+
const { publicInstallStatus } = await import('./core/dsh-cli.js');
|
|
119
|
+
return sendJson(res, 200, { ok: true, ...publicInstallStatus() });
|
|
120
|
+
}
|
|
121
|
+
case 'install': {
|
|
122
|
+
const id = String(body.id || '').trim();
|
|
123
|
+
if (!id)
|
|
124
|
+
return sendJson(res, 400, { ok: false, error: '缺少 id' });
|
|
125
|
+
const version = typeof body.version === 'string' ? body.version : undefined;
|
|
126
|
+
const result = await withMutationLock(() => installFromRegistry(id, cfg, { version }));
|
|
127
|
+
return sendJson(res, 200, { ok: true, ...result });
|
|
128
|
+
}
|
|
129
|
+
case 'uninstall': {
|
|
130
|
+
const target = String(body.pkg || '').trim();
|
|
131
|
+
if (!target)
|
|
132
|
+
return sendJson(res, 400, { ok: false, error: '缺少 pkg' });
|
|
133
|
+
const result = await withMutationLock(() => uninstallPlugin(target, cfg));
|
|
134
|
+
return sendJson(res, 200, { ok: true, ...result });
|
|
135
|
+
}
|
|
136
|
+
case 'upgrade': {
|
|
137
|
+
const target = String(body.pkg || '').trim();
|
|
138
|
+
if (!target)
|
|
139
|
+
return sendJson(res, 400, { ok: false, error: '缺少 pkg' });
|
|
140
|
+
const result = await withMutationLock(() => upgradePlugin(target, cfg));
|
|
141
|
+
return sendJson(res, 200, { ok: true, ...result });
|
|
142
|
+
}
|
|
143
|
+
case 'restart': {
|
|
144
|
+
if (!trustedRestartRequest(req)) {
|
|
145
|
+
return sendJson(res, 403, { ok: false, error: '拒绝跨源重启请求' });
|
|
146
|
+
}
|
|
147
|
+
const result = scheduleRestart(servingPort(req));
|
|
148
|
+
return sendJson(res, 200, { ok: true, ...result });
|
|
149
|
+
}
|
|
150
|
+
default:
|
|
151
|
+
return sendJson(res, 404, { ok: false, error: `未知 method: ${method}` });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
return sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function loadRegistrySafe(cfg, force) {
|
|
159
|
+
const { loadRegistry } = await import('./core/registry.js');
|
|
160
|
+
return loadRegistry(cfg, { force });
|
|
161
|
+
}
|
|
162
|
+
function boolArg(v) {
|
|
163
|
+
return v === true || v === 'true' || v === 1 || v === '1';
|
|
164
|
+
}
|
|
165
|
+
function readBody(req, maxBytes = 1 << 20) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const chunks = [];
|
|
168
|
+
let size = 0;
|
|
169
|
+
req.on('data', (c) => {
|
|
170
|
+
size += c.length;
|
|
171
|
+
if (size > maxBytes) {
|
|
172
|
+
reject(new Error('请求体过大'));
|
|
173
|
+
req.destroy();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
chunks.push(c);
|
|
177
|
+
});
|
|
178
|
+
req.on('end', () => {
|
|
179
|
+
if (!chunks.length)
|
|
180
|
+
return resolve({});
|
|
181
|
+
try {
|
|
182
|
+
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
resolve({});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
req.on('error', reject);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function sendJson(res, status, value) {
|
|
192
|
+
const body = JSON.stringify(value);
|
|
193
|
+
res.writeHead(status, {
|
|
194
|
+
'content-type': 'application/json; charset=utf-8',
|
|
195
|
+
'content-length': Buffer.byteLength(body),
|
|
196
|
+
'cache-control': 'no-store',
|
|
197
|
+
});
|
|
198
|
+
res.end(body);
|
|
199
|
+
}
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
2
|
+
import { installTimeoutMs } from './core/env.js';
|
|
3
|
+
import { installFromRegistry, listInstalledWithMeta, uninstallPlugin, upgradePlugin, withMutationLock } from './core/market.js';
|
|
4
|
+
import { scheduleRestart } from './core/restart.js';
|
|
5
|
+
export const CATEGORY_LABELS = {
|
|
6
|
+
market: '市场',
|
|
7
|
+
tools: '工具',
|
|
8
|
+
ui: '界面',
|
|
9
|
+
search: '搜索',
|
|
10
|
+
media: '多媒体',
|
|
11
|
+
other: '其他',
|
|
12
|
+
};
|
|
13
|
+
function cloneJson(value) {
|
|
14
|
+
return JSON.parse(JSON.stringify(value));
|
|
15
|
+
}
|
|
16
|
+
function matchInstalledByEntry(entry, installed) {
|
|
17
|
+
return installed.find((it) => {
|
|
18
|
+
if (entry.npm && (it.pkg === entry.npm || it.name === entry.npm))
|
|
19
|
+
return true;
|
|
20
|
+
if (entry.github && it.source === 'github') {
|
|
21
|
+
const m = /^github:([^#]+)/.exec(it.spec);
|
|
22
|
+
if (m && m[1] === entry.github)
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function registerTools(ctx, cfg) {
|
|
29
|
+
const timeoutMs = cfg.timeoutMs ?? 20_000;
|
|
30
|
+
ctx.tools.register(defineTool({
|
|
31
|
+
name: 'dshm_search',
|
|
32
|
+
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.',
|
|
33
|
+
parameters: {
|
|
34
|
+
query: { type: 'string', description: 'Main keyword, e.g. 主题 or 搜索. Optional.' },
|
|
35
|
+
category: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: `Optional first-level category: ${Object.keys(CATEGORY_LABELS).join(', ')}`,
|
|
38
|
+
},
|
|
39
|
+
limit: { type: 'number', description: 'Cards in this batch. Default all (registry is curated & small).' },
|
|
40
|
+
},
|
|
41
|
+
output: {
|
|
42
|
+
schema: { type: 'object', additionalProperties: true },
|
|
43
|
+
render: (_args, value) => [{ type: 'text', text: renderSearch(value) }],
|
|
44
|
+
presentationMeta: (_args, value) => ({ kind: 'dshm-search', ...value }),
|
|
45
|
+
},
|
|
46
|
+
presentCall: (args) => ({
|
|
47
|
+
card: 'generic',
|
|
48
|
+
title: `DSH 市场 · ${String(args.query || args.category || '浏览')}`,
|
|
49
|
+
kind: 'search',
|
|
50
|
+
content: [],
|
|
51
|
+
}),
|
|
52
|
+
presentResult: (_args, { isError, meta }) => ({
|
|
53
|
+
card: 'generic',
|
|
54
|
+
title: isError ? '市场搜索失败' : `DSH 市场 · ${meta?.items?.length ?? 0} 条`,
|
|
55
|
+
content: [],
|
|
56
|
+
}),
|
|
57
|
+
timeoutMs: timeoutMs + 5000,
|
|
58
|
+
async execute(args) {
|
|
59
|
+
const registry = await import('./core/registry.js');
|
|
60
|
+
const installed = await import('./core/installed.js');
|
|
61
|
+
const loaded = await registry.loadRegistry(cfg);
|
|
62
|
+
const inst = await installed.listInstalledPlugins();
|
|
63
|
+
const query = String(args.query || '').trim().toLowerCase();
|
|
64
|
+
const category = typeof args.category === 'string' && args.category ? args.category : null;
|
|
65
|
+
let items = loaded.registry.plugins.filter((e) => {
|
|
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);
|
|
72
|
+
});
|
|
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
|
+
return cloneJson({
|
|
77
|
+
query: String(args.query || ''),
|
|
78
|
+
category,
|
|
79
|
+
total,
|
|
80
|
+
items: items.map((e) => {
|
|
81
|
+
const i = matchInstalledByEntry(e, inst.items);
|
|
82
|
+
return {
|
|
83
|
+
id: e.id,
|
|
84
|
+
name: e.name,
|
|
85
|
+
description: e.description,
|
|
86
|
+
category: e.category,
|
|
87
|
+
tags: e.tags,
|
|
88
|
+
source: e.source,
|
|
89
|
+
npm: e.npm,
|
|
90
|
+
github: e.github,
|
|
91
|
+
homepage: e.homepage,
|
|
92
|
+
installed: Boolean(i),
|
|
93
|
+
installedPkg: i?.pkg,
|
|
94
|
+
installedVersion: i?.version,
|
|
95
|
+
};
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
}));
|
|
100
|
+
ctx.tools.register(defineTool({
|
|
101
|
+
name: 'dshm_list',
|
|
102
|
+
description: 'List plugins installed in the DSH web profile, annotated with 市场安装/非市场安装, sources, and outdated flags. Use when the user asks what plugins are installed or wants to manage local plugins.',
|
|
103
|
+
parameters: {},
|
|
104
|
+
output: {
|
|
105
|
+
schema: { type: 'object', additionalProperties: true },
|
|
106
|
+
render: (_args, value) => [{ type: 'text', text: renderList(value) }],
|
|
107
|
+
presentationMeta: (_args, value) => ({ kind: 'dshm-list', ...value }),
|
|
108
|
+
},
|
|
109
|
+
presentCall: () => ({ card: 'generic', title: '已装插件', kind: 'search', content: [] }),
|
|
110
|
+
presentResult: (_args, { isError, meta }) => ({
|
|
111
|
+
card: 'generic',
|
|
112
|
+
title: isError ? '列出失败' : `已装 · ${meta?.items?.length ?? 0} 个`,
|
|
113
|
+
content: [],
|
|
114
|
+
}),
|
|
115
|
+
timeoutMs: timeoutMs + 5000,
|
|
116
|
+
async execute() {
|
|
117
|
+
const result = await listInstalledWithMeta(cfg);
|
|
118
|
+
return cloneJson({
|
|
119
|
+
profileDir: result.profileDir,
|
|
120
|
+
others: result.others,
|
|
121
|
+
items: result.items.map((it) => ({
|
|
122
|
+
pkg: it.pkg,
|
|
123
|
+
name: it.name,
|
|
124
|
+
version: it.version,
|
|
125
|
+
source: it.source,
|
|
126
|
+
registryId: it.registryId ?? null,
|
|
127
|
+
latestVersion: it.latestVersion ?? null,
|
|
128
|
+
outdated: it.outdated,
|
|
129
|
+
})),
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
ctx.tools.register(defineTool({
|
|
134
|
+
name: 'dshm_install',
|
|
135
|
+
description: 'Install a plugin from the dsh-m registry into the current web profile after the user names one (装 dsh-skins / 安装 web-search). Pass the id from dshm_search results. npm 源锁定最新精确版本,github 源锁定 commit SHA。Do not print CLI commands. After success, tell the user it needs a restart of dsh web, and offer dshm_restart.',
|
|
136
|
+
parameters: {
|
|
137
|
+
id: { type: 'string', required: true, description: '收录 id from dshm_search, e.g. dsh-skins' },
|
|
138
|
+
version: { type: 'string', description: 'Optional exact semver (npm 源). Default latest.' },
|
|
139
|
+
},
|
|
140
|
+
output: {
|
|
141
|
+
schema: { type: 'object', additionalProperties: true },
|
|
142
|
+
render: (_args, value) => [{ type: 'text', text: renderInstall(value) }],
|
|
143
|
+
presentationMeta: (_args, value) => ({ kind: 'dshm-install', ...value }),
|
|
144
|
+
},
|
|
145
|
+
presentCall: (args) => ({ card: 'generic', title: `安装 · ${String(args.id || '')}`, kind: 'search', content: [] }),
|
|
146
|
+
presentResult: (_args, { isError, meta }) => ({
|
|
147
|
+
card: 'generic',
|
|
148
|
+
title: isError ? '安装失败' : `已安装 · ${meta?.pkg || ''}`,
|
|
149
|
+
content: [],
|
|
150
|
+
}),
|
|
151
|
+
timeoutMs: installTimeoutMs() + 60_000,
|
|
152
|
+
async execute(args) {
|
|
153
|
+
const id = String(args.id || '').trim();
|
|
154
|
+
if (!id)
|
|
155
|
+
throw new Error('缺少收录 id');
|
|
156
|
+
const version = typeof args.version === 'string' && args.version.trim() ? args.version.trim() : undefined;
|
|
157
|
+
return cloneJson(await withMutationLock(() => installFromRegistry(id, cfg, { version })));
|
|
158
|
+
},
|
|
159
|
+
}));
|
|
160
|
+
ctx.tools.register(defineTool({
|
|
161
|
+
name: 'dshm_uninstall',
|
|
162
|
+
description: 'Uninstall a DSH plugin from the web profile by package name (pkg from dshm_list). Confirm with the user BEFORE calling. Does not delete plugin data; reports leftover paths instead.',
|
|
163
|
+
parameters: {
|
|
164
|
+
pkg: { type: 'string', required: true, description: '包名 from dshm_list, e.g. dsh-web-search' },
|
|
165
|
+
},
|
|
166
|
+
output: {
|
|
167
|
+
schema: { type: 'object', additionalProperties: true },
|
|
168
|
+
render: (_args, value) => [{ type: 'text', text: renderUninstall(value) }],
|
|
169
|
+
presentationMeta: (_args, value) => ({ kind: 'dshm-uninstall', ...value }),
|
|
170
|
+
},
|
|
171
|
+
presentCall: (args) => ({ card: 'generic', title: `卸载 · ${String(args.pkg || '')}`, content: [] }),
|
|
172
|
+
presentResult: (_args, { isError, meta }) => ({
|
|
173
|
+
card: 'generic',
|
|
174
|
+
title: isError ? '卸载失败' : `已卸载 · ${meta?.pkg || ''}`,
|
|
175
|
+
content: [],
|
|
176
|
+
}),
|
|
177
|
+
timeoutMs: installTimeoutMs(),
|
|
178
|
+
async execute(args) {
|
|
179
|
+
const target = String(args.pkg || '').trim();
|
|
180
|
+
if (!target)
|
|
181
|
+
throw new Error('缺少 pkg');
|
|
182
|
+
return cloneJson(await withMutationLock(() => uninstallPlugin(target, cfg)));
|
|
183
|
+
},
|
|
184
|
+
}));
|
|
185
|
+
ctx.tools.register(defineTool({
|
|
186
|
+
name: 'dshm_outdated',
|
|
187
|
+
description: 'Check installed DSH plugins for newer versions (npm latest / GitHub HEAD). Use when the user asks about updates or 升级. Read-only.',
|
|
188
|
+
parameters: {},
|
|
189
|
+
output: {
|
|
190
|
+
schema: { type: 'object', additionalProperties: true },
|
|
191
|
+
render: (_args, value) => [{ type: 'text', text: renderOutdated(value) }],
|
|
192
|
+
presentationMeta: (_args, value) => ({ kind: 'dshm-outdated', ...value }),
|
|
193
|
+
},
|
|
194
|
+
presentCall: () => ({ card: 'generic', title: '检查更新', kind: 'search', content: [] }),
|
|
195
|
+
presentResult: (_args, { isError, meta }) => {
|
|
196
|
+
const out = meta;
|
|
197
|
+
const n = out?.items?.filter((it) => it.outdated).length ?? 0;
|
|
198
|
+
return { card: 'generic', title: isError ? '检查失败' : n ? `${n} 个可升级` : '全部最新', content: [] };
|
|
199
|
+
},
|
|
200
|
+
timeoutMs: timeoutMs + 10_000,
|
|
201
|
+
async execute() {
|
|
202
|
+
const result = await listInstalledWithMeta(cfg);
|
|
203
|
+
const items = result.items.map((it) => ({
|
|
204
|
+
pkg: it.pkg,
|
|
205
|
+
name: it.name,
|
|
206
|
+
version: it.version,
|
|
207
|
+
source: it.source,
|
|
208
|
+
latestVersion: it.latestVersion ?? null,
|
|
209
|
+
latestTag: it.latestTag ?? null,
|
|
210
|
+
outdated: it.outdated,
|
|
211
|
+
}));
|
|
212
|
+
return cloneJson({ items, outdatedCount: items.filter((it) => it.outdated).length });
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
ctx.tools.register(defineTool({
|
|
216
|
+
name: 'dshm_upgrade',
|
|
217
|
+
description: 'Upgrade an installed DSH plugin to the latest version (npm 拉最新精确版 / github 重新锁 HEAD)。pkg 来自 dshm_list 或 dshm_outdated。用户确认升级哪一个之后再调用。After success, tell the user it needs a restart, and offer dshm_restart.',
|
|
218
|
+
parameters: {
|
|
219
|
+
pkg: { type: 'string', required: true, description: '包名 from dshm_list / dshm_outdated' },
|
|
220
|
+
},
|
|
221
|
+
output: {
|
|
222
|
+
schema: { type: 'object', additionalProperties: true },
|
|
223
|
+
render: (_args, value) => [{ type: 'text', text: renderUpgrade(value) }],
|
|
224
|
+
presentationMeta: (_args, value) => ({ kind: 'dshm-upgrade', ...value }),
|
|
225
|
+
},
|
|
226
|
+
presentCall: (args) => ({ card: 'generic', title: `升级 · ${String(args.pkg || '')}`, content: [] }),
|
|
227
|
+
presentResult: (_args, { isError, meta }) => ({
|
|
228
|
+
card: 'generic',
|
|
229
|
+
title: isError ? '升级失败' : `已升级 · ${meta?.pkg || ''}`,
|
|
230
|
+
content: [],
|
|
231
|
+
}),
|
|
232
|
+
timeoutMs: installTimeoutMs() + 60_000,
|
|
233
|
+
async execute(args) {
|
|
234
|
+
const target = String(args.pkg || '').trim();
|
|
235
|
+
if (!target)
|
|
236
|
+
throw new Error('缺少 pkg');
|
|
237
|
+
return cloneJson(await withMutationLock(() => upgradePlugin(target, cfg)));
|
|
238
|
+
},
|
|
239
|
+
}));
|
|
240
|
+
ctx.tools.register(defineTool({
|
|
241
|
+
name: 'dshm_restart',
|
|
242
|
+
description: 'Restart DSH web so newly installed/uninstalled/upgraded plugins take effect. ONLY call after the user agrees (用户同意重启后). The page reloads automatically after the service comes back.',
|
|
243
|
+
parameters: {},
|
|
244
|
+
output: {
|
|
245
|
+
schema: { type: 'object', additionalProperties: true },
|
|
246
|
+
render: (_args, value) => [{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: `已请求重启 DSH web(via ${value.via})。服务几秒内恢复,之后让用户刷新页面即可。对用户最多一句短话。`,
|
|
249
|
+
}],
|
|
250
|
+
presentationMeta: (_args, value) => ({ kind: 'dshm-restart', ...value }),
|
|
251
|
+
},
|
|
252
|
+
presentCall: () => ({ card: 'generic', title: '重启 DSH Web', content: [] }),
|
|
253
|
+
presentResult: (_args, { isError }) => ({
|
|
254
|
+
card: 'generic',
|
|
255
|
+
title: isError ? '重启失败' : '已请求重启',
|
|
256
|
+
content: [],
|
|
257
|
+
}),
|
|
258
|
+
timeoutMs: 15_000,
|
|
259
|
+
async execute() {
|
|
260
|
+
return cloneJson(scheduleRestart(null));
|
|
261
|
+
},
|
|
262
|
+
}));
|
|
263
|
+
ctx.inject(['systemPrompt'], (c) => {
|
|
264
|
+
const prompt = c.systemPrompt;
|
|
265
|
+
prompt.section({
|
|
266
|
+
name: 'tool:dshm',
|
|
267
|
+
order: 211,
|
|
268
|
+
text: [
|
|
269
|
+
'Finding / recommending / browsing DSH plugins (插件) in the personal marketplace: you MUST call dshm_search, never web_search or bash. One call per user message; extract a real keyword. After cards appear, reply with AT MOST one short sentence. Do not print install commands.',
|
|
270
|
+
`Plugin categories: ${Object.entries(CATEGORY_LABELS).map(([k, v]) => `${k}=${v}`).join(', ')}.`,
|
|
271
|
+
'Install only after the user names a card: dshm_install with its id. Then one short sentence mentioning the restart requirement; offer dshm_restart.',
|
|
272
|
+
'For installed plugins, call dshm_list / dshm_outdated. Upgrade only after the user confirms which one: dshm_upgrade. Uninstall only after confirmation: dshm_uninstall.',
|
|
273
|
+
'dshm_restart only after the user agrees to restart; afterwards tell them to refresh once the page recovers.',
|
|
274
|
+
].join(' '),
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function renderSearch(out) {
|
|
279
|
+
if (!out.items?.length)
|
|
280
|
+
return '收录清单中没有匹配的插件。对用户只说一句:没找到,可以换个词再搜。不要写长文。';
|
|
281
|
+
const lines = out.items.map((it, i) => {
|
|
282
|
+
const inst = it.installed ? `(已安装 v${it.installedVersion || '?'})` : '';
|
|
283
|
+
return `${i + 1}. ${it.name} · ${it.id}${inst} · ${CATEGORY_LABELS[it.category] || it.category}`;
|
|
284
|
+
});
|
|
285
|
+
return [
|
|
286
|
+
`插件卡片已展示 ${out.items.length}${out.total && out.total > out.items.length ? `/${out.total}` : ''} 条(内部序号,禁止复述给用户):`,
|
|
287
|
+
lines.join('\n'),
|
|
288
|
+
'对用户最多回一句短话。禁止清单和长文。用户点名安装时才调 dshm_install(id)。',
|
|
289
|
+
].join('\n');
|
|
290
|
+
}
|
|
291
|
+
function renderList(out) {
|
|
292
|
+
if (!out.items?.length)
|
|
293
|
+
return 'web profile 还没有安装任何 dsh 插件。对用户一句短话即可。';
|
|
294
|
+
const lines = out.items.map((it, i) => {
|
|
295
|
+
const marks = [
|
|
296
|
+
it.registryId ? '市场' : '非市场',
|
|
297
|
+
it.outdated && it.latestVersion ? `可升级 → v${it.latestVersion}` : null,
|
|
298
|
+
].filter(Boolean).join(',');
|
|
299
|
+
return `${i + 1}. ${it.name} (${it.pkg}) v${it.version || '?'} · ${it.source}${marks ? ` · ${marks}` : ''}`;
|
|
300
|
+
});
|
|
301
|
+
return [
|
|
302
|
+
`已安装 ${out.items.length} 个插件(内部序号,禁止复述给用户):`,
|
|
303
|
+
lines.join('\n'),
|
|
304
|
+
'对用户最多回一句短话。管理动作:dshm_upgrade / dshm_uninstall(先与用户确认)。',
|
|
305
|
+
].join('\n');
|
|
306
|
+
}
|
|
307
|
+
function renderInstall(out) {
|
|
308
|
+
const extra = out.usedAllowAllBuilds ? '注意:该插件执行了构建脚本(已按策略放行)。' : '';
|
|
309
|
+
return `✅ ${out.pkg} 已安装(${out.spec})。${extra}需要重启 DSH Web 生效——告知用户并询问是否 dshm_restart。不要打印安装命令。`;
|
|
310
|
+
}
|
|
311
|
+
function renderUninstall(out) {
|
|
312
|
+
const parts = [`✅ ${out.pkg} 已卸载。`];
|
|
313
|
+
if (out.liveDisabled)
|
|
314
|
+
parts.push('运行中的界面已先下线。');
|
|
315
|
+
if (out.leftovers?.length)
|
|
316
|
+
parts.push(`疑似残留数据(未删除,仅供知晓):${out.leftovers.join('、')}`);
|
|
317
|
+
parts.push('需要重启生效——询问是否 dshm_restart。');
|
|
318
|
+
return parts.join(' ');
|
|
319
|
+
}
|
|
320
|
+
function renderOutdated(out) {
|
|
321
|
+
const outdated = (out.items || []).filter((it) => it.outdated);
|
|
322
|
+
if (!out.items?.length)
|
|
323
|
+
return 'web profile 没有已装插件。';
|
|
324
|
+
if (!outdated.length)
|
|
325
|
+
return `全部 ${out.items.length} 个插件均已是最新版本。对用户一句短话。`;
|
|
326
|
+
const lines = outdated.map((it) => `${it.name} (${it.pkg}):v${it.version} → ${it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : '最新')}`);
|
|
327
|
+
return [
|
|
328
|
+
`${outdated.length}/${out.items.length} 个插件可升级:`,
|
|
329
|
+
lines.join('\n'),
|
|
330
|
+
'询问用户要升级哪个,确认后调 dshm_upgrade(pkg)。',
|
|
331
|
+
].join('\n');
|
|
332
|
+
}
|
|
333
|
+
function renderUpgrade(out) {
|
|
334
|
+
const from = out.fromVersion ? `v${out.fromVersion} → ` : '';
|
|
335
|
+
const to = out.version ? `v${out.version}` : out.sha ? out.sha.slice(0, 7) : '最新';
|
|
336
|
+
const extra = out.usedAllowAllBuilds ? '注意:该插件执行了构建脚本。' : '';
|
|
337
|
+
return `✅ ${out.pkg} 已升级(${from}${to})。${extra}需要重启生效——询问是否 dshm_restart。`;
|
|
338
|
+
}
|
|
339
|
+
function clamp(n, min, max) {
|
|
340
|
+
return Math.min(max, Math.max(min, n));
|
|
341
|
+
}
|