dsh-plugin 1.6.5 → 1.6.7

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.
Files changed (53) hide show
  1. package/README.en.md +11 -11
  2. package/README.md +11 -11
  3. package/client/client.js +421 -390
  4. package/client/client.js.map +1 -1
  5. package/lib/http/catalog.js +132 -0
  6. package/lib/http/diagnostics.js +201 -0
  7. package/lib/http/install.js +399 -0
  8. package/lib/http/logs.js +164 -0
  9. package/lib/http/routes.js +19 -1263
  10. package/lib/http/settings.js +77 -0
  11. package/lib/http/shared.js +169 -0
  12. package/lib/http/task.js +208 -0
  13. package/lib/services/install/install.js +1 -1
  14. package/lib/services/install/package-entry.js +59 -8
  15. package/lib/services/install/preflight.js +47 -17
  16. package/lib/services/install/task-queue.js +75 -31
  17. package/lib/services/probe.js +1 -1
  18. package/lib/types/http/catalog.d.ts +10 -0
  19. package/lib/types/http/diagnostics.d.ts +4 -0
  20. package/lib/types/http/install.d.ts +11 -0
  21. package/lib/types/http/logs.d.ts +2 -0
  22. package/lib/types/http/routes.d.ts +15 -20
  23. package/lib/types/http/settings.d.ts +2 -0
  24. package/lib/types/http/shared.d.ts +44 -0
  25. package/lib/types/http/task.d.ts +3 -0
  26. package/lib/types/services/install/install.d.ts +1 -1
  27. package/lib/types/services/install/package-entry.d.ts +42 -10
  28. package/lib/types/services/install/preflight.d.ts +9 -1
  29. package/lib/types/services/install/task-queue.d.ts +6 -4
  30. package/package.json +1 -1
  31. package/src/client/components/PluginHubSection.tsx +3 -0
  32. package/src/client/components/modals/NotificationsModal.tsx +34 -18
  33. package/src/client/components/modals/modals.tsx +38 -28
  34. package/src/client/components/views/InstalledView.tsx +23 -11
  35. package/src/client/hooks/useCatalog.ts +1 -1
  36. package/src/client/locales.ts +28 -12
  37. package/src/client/logic/failures.ts +36 -157
  38. package/src/client/logic/urls.ts +75 -4
  39. package/src/client/types.ts +1 -1
  40. package/src/server/http/catalog.ts +143 -0
  41. package/src/server/http/diagnostics.ts +193 -0
  42. package/src/server/http/install.ts +391 -0
  43. package/src/server/http/logs.ts +149 -0
  44. package/src/server/http/routes.ts +22 -1222
  45. package/src/server/http/settings.ts +73 -0
  46. package/src/server/http/shared.ts +191 -0
  47. package/src/server/http/task.ts +194 -0
  48. package/src/server/services/install/install-types.ts +9 -1
  49. package/src/server/services/install/install.ts +11 -1
  50. package/src/server/services/install/package-entry.ts +72 -9
  51. package/src/server/services/install/preflight.ts +47 -18
  52. package/src/server/services/install/task-queue.ts +73 -34
  53. package/src/server/services/probe.ts +1 -1
@@ -0,0 +1,132 @@
1
+ /**
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Catalog proxy routes: server-side caching of the directory/stats payloads
7
+ * and profile-level installed-package reads shared with the install/task modules.
8
+ */
9
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
+ import { homedir } from 'node:os';
11
+ import { dirname, join } from 'node:path';
12
+ import { githubRepoOf } from '../services/install/install.js';
13
+ import { loadSettings } from '../services/settings.js';
14
+ import { fetchViaCurl, systemProxy } from '../services/probe.js';
15
+ import { CATALOG_CACHE_TTL_MS, requireMethod, sendJson } from './shared.js';
16
+ function catalogCacheFile(profile, key) {
17
+ return join(profileDirectory(profile), 'cache', `catalog-${key}.json`);
18
+ }
19
+ /** 读缓存:文件存在、JSON 合法、未超过 maxAgeMs → 返回原始响应字符串;否则 null(视为 miss)。
20
+ * maxAgeMs 传 Infinity 即「任意年龄的缓存」,供实时拉取失败时兜底使用。 */
21
+ function readCatalogCache(file, maxAgeMs) {
22
+ try {
23
+ const entry = JSON.parse(readFileSync(file, 'utf8'));
24
+ if (entry && typeof entry.at === 'number' && typeof entry.body === 'string'
25
+ && Date.now() - entry.at < maxAgeMs) {
26
+ return entry.body;
27
+ }
28
+ }
29
+ catch {
30
+ // 文件缺失 / 解析失败 / 字段不完整 → miss,重新拉取覆盖写盘
31
+ }
32
+ return null;
33
+ }
34
+ /** 写缓存:目录不存在则创建;写失败静默忽略(缓存只是加速,不影响功能)。 */
35
+ function writeCatalogCache(file, body) {
36
+ try {
37
+ mkdirSync(dirname(file), { recursive: true });
38
+ writeFileSync(file, JSON.stringify({ at: Date.now(), body }));
39
+ }
40
+ catch {
41
+ // 磁盘只读/无权限等极端情况:忽略,继续走直连
42
+ }
43
+ }
44
+ export function profileDirectory(profile) {
45
+ return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'profiles', profile);
46
+ }
47
+ /** Read non-official dependencies installed into one profile. */
48
+ export function readInstalled(profile) {
49
+ try {
50
+ const manifest = JSON.parse(readFileSync(join(profileDirectory(profile), 'package.json'), 'utf8'));
51
+ return Object.fromEntries(Object.entries(manifest.dependencies ?? {}).filter(([name]) => !name.startsWith('@deepseek-ai/')));
52
+ }
53
+ catch {
54
+ return {};
55
+ }
56
+ }
57
+ /** 读已装依赖实际落在磁盘上的仓库身份(`owner/repo`):解析 node_modules/<name>/package.json
58
+ * 的 repository 字段。已装 spec 可能是纯版本号(丢了仓库身份),但真实包元数据仍指向来源仓库,
59
+ * 据此可区分「同名依赖其实是同一仓库的残留(应转 update)」还是「跨仓库包名冲突(应报错)」。
60
+ * 读不到 / 解析不出返回 null(交给调用方按冲突处理)。 */
61
+ export function installedRepoOf(profile, name) {
62
+ try {
63
+ const pkg = JSON.parse(readFileSync(join(profileDirectory(profile), 'node_modules', name, 'package.json'), 'utf8'));
64
+ const repo = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
65
+ return repo ? githubRepoOf(repo) : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ export function registerCatalogRoutes(webServer, profile) {
72
+ return [
73
+ {
74
+ kind: 'exact',
75
+ path: '/dsh-plugin-hub/catalog',
76
+ handler: async (request, response) => {
77
+ if (!requireMethod(request, response, 'GET'))
78
+ return;
79
+ // 目录/统计数据服务端代理:浏览器不直连 dsh-plugin.org,统一经此路由
80
+ // 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理口径,
81
+ // 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
82
+ const url = new URL(request.url ?? '/', 'http://localhost');
83
+ const settings = loadSettings(profile);
84
+ // 代理优先级与安装/诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连
85
+ const proxy = settings.proxy !== ''
86
+ ? settings.proxy
87
+ : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
88
+ const isStats = url.searchParams.get('stats') === '1';
89
+ const lang = url.searchParams.get('lang') === 'en' ? 'en' : 'zh';
90
+ const target = isStats
91
+ ? 'https://api.dsh-plugin.org/stats.json'
92
+ : `https://api.dsh-plugin.org/plugins.${lang}.json`;
93
+ // 1 小时本地缓存:命中直接返回(跨重启生效,插件市场秒开),未命中才走 curl 拉远程
94
+ const cacheFile = catalogCacheFile(profile, isStats ? 'stats' : `plugins-${lang}`);
95
+ const cached = readCatalogCache(cacheFile, CATALOG_CACHE_TTL_MS);
96
+ if (cached !== null) {
97
+ try {
98
+ sendJson(response, 200, JSON.parse(cached));
99
+ return;
100
+ }
101
+ catch {
102
+ // 缓存正文损坏:忽略,走重新拉取并覆盖写盘
103
+ }
104
+ }
105
+ const r = await fetchViaCurl(target, proxy, 20000);
106
+ if (!r.ok || r.body === '') {
107
+ // 实时拉取失败:退回任意年龄的本地缓存(上次成功数据),
108
+ // 避免证书吊销受阻 / 网络抖动时整个插件市场打不开 —— 数据可能过期,但好过空白。
109
+ const stale = readCatalogCache(cacheFile, Number.POSITIVE_INFINITY);
110
+ if (stale !== null) {
111
+ try {
112
+ sendJson(response, 200, JSON.parse(stale));
113
+ return;
114
+ }
115
+ catch {
116
+ // 兜底缓存也损坏:落到下面的 502
117
+ }
118
+ }
119
+ sendJson(response, 502, { error: 'catalog fetch failed' });
120
+ return;
121
+ }
122
+ try {
123
+ sendJson(response, 200, JSON.parse(r.body));
124
+ writeCatalogCache(cacheFile, r.body);
125
+ }
126
+ catch {
127
+ sendJson(response, 502, { error: 'catalog fetch returned invalid JSON' });
128
+ }
129
+ },
130
+ },
131
+ ];
132
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * DSH Plugin Hub — the community plugin marketplace for DeepSeek Harness.
3
+ * Website: https://dsh-plugin.org
4
+ * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
+ *
6
+ * Diagnostics routes: streaming connectivity self-check, proxy verification,
7
+ * and the host environment snapshot shared with the task module's /env route.
8
+ */
9
+ import { readFileSync } from 'node:fs';
10
+ import { homedir, release } from 'node:os';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { spawnSync } from 'node:child_process';
13
+ import { loadSettings } from '../services/settings.js';
14
+ import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
15
+ import { appendLog } from '../services/log.js';
16
+ import { readJsonBody, requireTrustedPost, sendJson } from './shared.js';
17
+ /** 宿主 dsh CLI 版本:优先直接跑 `dsh --version`(与 pnpm/npm/git 同款同步探测,1500ms 超时兜底,
18
+ * 绝不挂起);拿不到再退回从入口 `process.argv[1]` 向上找 package.json(dsh 不在 PATH 时兜底)。 */
19
+ function hostDshVersion() {
20
+ const viaCli = toolVersion('dsh', ['--version']);
21
+ if (viaCli)
22
+ return viaCli;
23
+ const entry = process.argv[1];
24
+ if (entry === undefined)
25
+ return null;
26
+ let dir = dirname(resolve(entry));
27
+ for (let depth = 0; depth < 4; depth++) {
28
+ try {
29
+ const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
30
+ const name = manifest.name ?? '';
31
+ if (typeof manifest.version === 'string' && (name === 'dsh' || name.includes('dsh') || name.includes('harness'))) {
32
+ return manifest.version;
33
+ }
34
+ }
35
+ catch {
36
+ /* not a package root — keep walking up */
37
+ }
38
+ const parent = dirname(dir);
39
+ if (parent === dir)
40
+ break;
41
+ dir = parent;
42
+ }
43
+ return null;
44
+ }
45
+ /** 探测命令行工具的版本号(pnpm --version / npm --version / git --version):
46
+ * 未安装 / 不在 PATH / 超时一律返回 null —— issue 环境快照如实写 unknown,绝不挂起。
47
+ * (同步探测仅用于启动期/请求期的低频率 /env 快照,安装通道不经过这里。) */
48
+ function toolVersion(cmd, args) {
49
+ try {
50
+ const r = spawnSync(cmd, args, { timeout: 1500, encoding: 'utf8' });
51
+ if (r.error || r.status !== 0)
52
+ return null;
53
+ const v = (r.stdout ?? '').trim().split(/\r?\n/)[0];
54
+ return v || null;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ /** 宿主环境快照:提交 bug 时随 issue 附上,便于作者复现(前端 /env 端点返回)。 */
61
+ export function hostEnv(profile) {
62
+ return {
63
+ dshVersion: hostDshVersion(),
64
+ nodeVersion: process.version,
65
+ pnpmVersion: toolVersion('pnpm', ['--version']),
66
+ npmVersion: toolVersion('npm', ['--version']),
67
+ gitVersion: toolVersion('git', ['--version']),
68
+ platform: process.platform,
69
+ arch: process.arch,
70
+ release: release(),
71
+ profile,
72
+ dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
73
+ };
74
+ }
75
+ export function registerDiagnosticsRoutes(webServer, profile) {
76
+ return [
77
+ {
78
+ kind: 'exact',
79
+ path: '/dsh-plugin-hub/diagnostics',
80
+ handler: async (request, response) => {
81
+ if (!requireTrustedPost(request, response))
82
+ return;
83
+ // 连通性自检:npm registry(镜像/官方)/ GitHub API / 目录站点,逐路探测并测速。
84
+ // 与安装通道一一对应 —— npm 反查与 npm 安装走 registry、git 源走 github、目录数据走 dsh-plugin.org。
85
+ // 流式(NDJSON)逐条上报:每个探测先发命令行,完成后发结果行,客户端以黑窗口终端实时展示。
86
+ // 可选 body { key }:只重测指定通道(页面列表「重测」单项时用),缺省全量串行探测。
87
+ let onlyKey = '';
88
+ try {
89
+ const body = await readJsonBody(request);
90
+ if (body !== null && typeof body === 'object' && typeof body.key === 'string') {
91
+ onlyKey = body.key;
92
+ }
93
+ }
94
+ catch {
95
+ // 空 body / 非 JSON:视为全量探测
96
+ }
97
+ const settings = loadSettings(profile);
98
+ // npm 通道:设置里配置了镜像源就探测该镜像(安装通道吃同一个 registry);
99
+ // 未配置(空串)时探测官方源作为「跟随本机 npm 配置」的基础连通性参考。
100
+ const registry = settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org';
101
+ // 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
102
+ // 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
103
+ // npm 行 display:配置了镜像就显示镜像地址;未配置置空 —— 客户端显示「未配置(跟随本机 npm 配置)」。
104
+ const allChecks = [
105
+ { key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? registry : '', cmd: `npm view dsh-plugin version --registry ${registry}` },
106
+ // GitHub 行用真实 git 克隆握手(git ls-remote)打 dshplugin/hello-dsh 小仓库:
107
+ // 「网页能打开」和「git 能克隆」是两码事,握手成功才算通道通;小仓库秒级完成,不打 17MB 的 dsh-plugin-hub。
108
+ { key: 'github', url: 'https://github.com/dshplugin/hello-dsh', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/hello-dsh', git: true },
109
+ { key: 'catalog', url: 'https://api.dsh-plugin.org/stats.json', display: '', cmd: 'curl -s https://api.dsh-plugin.org/stats.json' },
110
+ ];
111
+ // 配置了 HTTP 代理:追加一行代理诊断 —— 用该代理打 github.com(安装通道真实访问的地址),
112
+ // 验证「代理能不能把请求带出去」。与安装同口径:curl 子进程注入该代理 env。
113
+ // 设置里未配置代理时不显示该行(跟随系统代理/直连,无「代理本身」可验证)。
114
+ if (settings.proxy !== '') {
115
+ allChecks.push({ key: 'proxy', url: 'https://github.com', display: settings.proxy, cmd: `curl -s -o /dev/null -x ${settings.proxy} https://github.com`, proxy: settings.proxy });
116
+ }
117
+ const checks = onlyKey !== '' ? allChecks.filter((c) => c.key === onlyKey) : allChecks;
118
+ response.writeHead(200, {
119
+ 'content-type': 'application/x-ndjson; charset=utf-8',
120
+ 'cache-control': 'no-store',
121
+ });
122
+ const send = (line) => { response.write(`${JSON.stringify(line)}\n`); };
123
+ // 探测走代理的优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 环境变量 HTTPS_PROXY → 直连。
124
+ // 与安装通道一致(npm/git 靠 HTTP_PROXY 环境变量走代理),且自动跟随系统代理 ——
125
+ // 浏览器挂着代理能开、诊断就能测通,不用用户手动把代理填进设置。
126
+ // proxy 行例外:它验证的正是「设置里配的那个代理」,必须显式用它(而非 effectiveProxy)去探测。
127
+ const effectiveProxy = settings.proxy !== ''
128
+ ? settings.proxy
129
+ : (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
130
+ const probe = (url, proxy) => probeUrl(url, proxy ?? effectiveProxy, 6000);
131
+ // 逐路串行:一条命令一条结果地推送给终端,保持「程序在跑」的真实感
132
+ const results = [];
133
+ for (const c of checks) {
134
+ send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display });
135
+ // git 行走真实克隆握手(gitLsRemote),其余走 curl 探测,两路都带超时兜底
136
+ const r = c.git
137
+ ? await gitLsRemote(c.url, c.proxy ?? effectiveProxy, 6000)
138
+ : await probe(c.url, c.proxy);
139
+ results.push({ key: c.key, ok: r.ok });
140
+ // git 行的 status 是退出码,客户端 git 行只显示 OK / 不可达,不显示成 HTTP 码。
141
+ // reason 透出失败细分原因(如 Windows 证书吊销受阻),客户端据此给对症提示而非笼统「不可达」。
142
+ send({
143
+ type: r.ok ? 'ok' : 'fail',
144
+ key: c.key,
145
+ display: c.display,
146
+ ms: r.ms,
147
+ status: c.git ? null : r.status,
148
+ reason: r.ok ? null : (r.reason ?? null),
149
+ });
150
+ }
151
+ send({ type: 'end', at: Date.now() });
152
+ response.end();
153
+ // 系统日志:诊断汇总(全部通过 / 存在不可达)
154
+ const failed = results.filter((r) => !r.ok);
155
+ appendLog(profile, {
156
+ at: Date.now(),
157
+ level: failed.length === 0 ? 'success' : 'error',
158
+ category: 'diagnostics',
159
+ event: 'diagnostics.done',
160
+ message: failed.length === 0
161
+ ? `连通性检测通过(${results.length} 个通道)`
162
+ : `连通性检测:${failed.length}/${results.length} 个通道不可达`,
163
+ });
164
+ },
165
+ },
166
+ {
167
+ kind: 'exact',
168
+ path: '/dsh-plugin-hub/proxy-check',
169
+ handler: async (request, response) => {
170
+ if (!requireTrustedPost(request, response))
171
+ return;
172
+ // 代理连通性校验:用待测代理打 dshplugin/hello-dsh 真实仓库页(权威可达目标),
173
+ // 供设置页输入代理时实时反馈「这个地址通不通」。不通过也允许保存 ——
174
+ // 用户可能是先填地址后开代理,因此这里只报告探测结果、不拦截保存。
175
+ let proxy = '';
176
+ let target = 'https://github.com/dshplugin/hello-dsh';
177
+ try {
178
+ const body = await readJsonBody(request);
179
+ if (body !== null && typeof body === 'object') {
180
+ const b = body;
181
+ if (typeof b.proxy === 'string')
182
+ proxy = b.proxy.trim();
183
+ if (typeof b.target === 'string' && b.target.trim() !== '')
184
+ target = b.target.trim();
185
+ }
186
+ }
187
+ catch {
188
+ // 空 body / 非 JSON:按默认目标探测
189
+ }
190
+ if (proxy === '') {
191
+ sendJson(response, 200, { ok: false, ms: null, status: null, target, reason: 'empty' });
192
+ return;
193
+ }
194
+ // 探测走该代理本身(不是 effectiveProxy):验证的就是用户填的这个地址。
195
+ // reason 带上失败细分原因(如证书吊销受阻),设置页才能提示「不是代理地址的问题」。
196
+ const r = await probeUrl(target, proxy, 6000);
197
+ sendJson(response, 200, { ok: r.ok, ms: r.ms, status: r.status, target, reason: r.reason ?? null });
198
+ },
199
+ },
200
+ ];
201
+ }