dsh-plugin 1.4.12 → 1.4.14
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 +11 -11
- package/README.md +11 -11
- package/client/client.js +374 -303
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +17 -10
- package/lib/services/install/npm-resolve.js +29 -13
- package/lib/services/install/preflight.js +5 -0
- package/lib/services/install/release-target.js +38 -0
- package/lib/services/profile/profile.js +6 -0
- package/lib/types/services/install/npm-resolve.d.ts +22 -0
- package/lib/types/services/install/release-target.d.ts +28 -0
- package/package.json +1 -1
- package/src/client/components/modals/AboutModal.tsx +18 -2
- package/src/client/components/modals/NotificationsModal.tsx +11 -1
- package/src/client/components/modals/modals.tsx +12 -1
- package/src/client/locales.ts +7 -0
- package/src/client/logic/failures.ts +31 -5
- package/src/client/logic/install-command.ts +18 -8
- package/src/client/logic/urls.ts +1 -0
- package/src/server/http/routes.ts +17 -10
- package/src/server/services/install/npm-resolve.ts +41 -13
- package/src/server/services/install/preflight.ts +4 -0
- package/src/server/services/install/release-target.ts +46 -0
- package/src/server/services/profile/profile.ts +5 -0
|
@@ -18,6 +18,8 @@ const cache = new Map<string, string | null>()
|
|
|
18
18
|
|
|
19
19
|
/** 单次 registry 查询超时(ms):慢网络下失败返回 null,不阻塞安装流程。 */
|
|
20
20
|
const REQUEST_TIMEOUT_MS = 8000
|
|
21
|
+
/** npm search 是相关性搜索而非 repository 精确索引;拉满单页结果后再本地做严格 repo 过滤。 */
|
|
22
|
+
const SEARCH_RESULT_LIMIT = 250
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* 反查 repo(`owner/repo`)对应的官方 npm 包名;未命中、网络异常或超时返回 null。
|
|
@@ -39,13 +41,49 @@ export function resolveNpmPackage(repo: string, registry = ''): Promise<string |
|
|
|
39
41
|
return name.then((found) => found ?? null)
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
type NpmSearchPackage = {
|
|
45
|
+
name?: unknown
|
|
46
|
+
keywords?: unknown
|
|
47
|
+
links?: { repository?: unknown }
|
|
48
|
+
repository?: { url?: unknown }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type NpmSearchObject = { package?: NpmSearchPackage }
|
|
52
|
+
type NpmSearchResponse = { objects?: NpmSearchObject[] }
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Pick the installable npm package for one GitHub repository. npm search is a
|
|
56
|
+
* relevance-ranked text search, not an exact repository index, so first filter
|
|
57
|
+
* by repository URL. If a monorepo publishes both a CLI/helper package and a
|
|
58
|
+
* DSH plugin, prefer the package that explicitly advertises the `dsh-plugin`
|
|
59
|
+
* keyword instead of whichever result npm happened to rank first.
|
|
60
|
+
*/
|
|
61
|
+
export function selectNpmPackageForRepo(objects: NpmSearchObject[], repo: string): string | null {
|
|
62
|
+
const needle = `github.com/${repo}`.toLowerCase()
|
|
63
|
+
const matches: Array<{ name: string; isDshPlugin: boolean }> = []
|
|
64
|
+
|
|
65
|
+
for (const obj of objects) {
|
|
66
|
+
const pkg = obj.package
|
|
67
|
+
if (typeof pkg?.name !== 'string' || pkg.name === '') continue
|
|
68
|
+
const repoUrl = String(pkg.repository?.url ?? pkg.links?.repository ?? '')
|
|
69
|
+
if (!repoUrl.toLowerCase().includes(needle)) continue
|
|
70
|
+
const keywords = Array.isArray(pkg.keywords) ? pkg.keywords : []
|
|
71
|
+
matches.push({
|
|
72
|
+
name: pkg.name,
|
|
73
|
+
isDshPlugin: keywords.some((keyword) => typeof keyword === 'string' && keyword.toLowerCase() === 'dsh-plugin'),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return matches.find((candidate) => candidate.isDshPlugin)?.name ?? matches[0]?.name ?? null
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
/** 用 npm search 接口按 repository 地址反查,并做铁证校验(包元数据必须指向该仓库)。
|
|
43
81
|
* 返回:包名 = 命中;null = 200 响应下确认无匹配;undefined = 本次未能确认(网络/超时/解析失败)。 */
|
|
44
82
|
function searchRepo(repo: string, registry: string): Promise<string | null | undefined> {
|
|
45
83
|
const [owner, name] = repo.split('/')
|
|
46
84
|
if (owner === undefined || name === undefined || name === '') return Promise.resolve(null)
|
|
47
85
|
const base = registry === '' ? 'https://registry.npmjs.org' : registry.replace(/\/+$/, '')
|
|
48
|
-
const url = `${base}/-/v1/search?text=${encodeURIComponent(`repository:${owner}/${name}`)}&size
|
|
86
|
+
const url = `${base}/-/v1/search?text=${encodeURIComponent(`repository:${owner}/${name}`)}&size=${SEARCH_RESULT_LIMIT}`
|
|
49
87
|
return new Promise((resolve) => {
|
|
50
88
|
const req = get(url, { timeout: REQUEST_TIMEOUT_MS }, (res) => {
|
|
51
89
|
if (res.statusCode !== 200) {
|
|
@@ -57,18 +95,8 @@ function searchRepo(repo: string, registry: string): Promise<string | null | und
|
|
|
57
95
|
res.on('data', (chunk: Buffer) => { body += chunk.toString() })
|
|
58
96
|
res.on('end', () => {
|
|
59
97
|
try {
|
|
60
|
-
const data = JSON.parse(body) as
|
|
61
|
-
|
|
62
|
-
for (const obj of data.objects ?? []) {
|
|
63
|
-
const pkg = obj.package
|
|
64
|
-
if (typeof pkg?.name !== 'string' || pkg.name === '') continue
|
|
65
|
-
const repoUrl = String(pkg.repository?.url ?? pkg.links?.repository ?? '')
|
|
66
|
-
if (repoUrl.toLowerCase().includes(needle)) {
|
|
67
|
-
resolve(pkg.name)
|
|
68
|
-
return
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
resolve(null)
|
|
98
|
+
const data = JSON.parse(body) as NpmSearchResponse
|
|
99
|
+
resolve(selectNpmPackageForRepo(data.objects ?? [], `${owner}/${name}`))
|
|
72
100
|
} catch {
|
|
73
101
|
resolve(undefined)
|
|
74
102
|
}
|
|
@@ -18,6 +18,7 @@ import { join } from 'node:path'
|
|
|
18
18
|
import { pipeline } from 'node:stream/promises'
|
|
19
19
|
import { promisify } from 'node:util'
|
|
20
20
|
import { githubRepoOf } from '../profile/profile.ts'
|
|
21
|
+
import { githubReleaseTarget } from './release-target.ts'
|
|
21
22
|
import { resolvePackageEntry } from './package-entry.ts'
|
|
22
23
|
|
|
23
24
|
const execFileAsync = promisify(execFile)
|
|
@@ -37,6 +38,9 @@ export interface PreflightResult {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
export async function preflightTarget(target: string): Promise<PreflightResult> {
|
|
41
|
+
// release 包自带包根:拿 monorepo HEAD 的源码分发去校验它没有意义,会把完整产物
|
|
42
|
+
// 误判成缺入口文件而拦下。该包的真实性交给安装本身与装后 verifyInstalledEntry 兜底。
|
|
43
|
+
if (githubReleaseTarget(target) !== null) return { ok: true, missing: null, name: null }
|
|
40
44
|
const source = githubRepoOf(target)
|
|
41
45
|
if (source === null) return { ok: true, missing: null, name: null }
|
|
42
46
|
const [owner, repo] = source.split('/')
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
* 预构建 GitHub release 安装目标(目录权威命令 → 可安装直链)。
|
|
7
|
+
* 部分插件(如 loopx)把构建产物发布成 GitHub release 的 .tgz,仓库 HEAD 反而是
|
|
8
|
+
* monorepo 根、走 git 直装会因缺入口文件失败;目录下发的权威安装命令直接指向该
|
|
9
|
+
* release 包。这里把「目录命令」解析成安全的 release 目标,供客户端选路与服务端
|
|
10
|
+
* 校验共用(单一事实来源,避免两端口径漂移)。
|
|
11
|
+
*
|
|
12
|
+
* 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
|
|
13
|
+
* 目录命令是展示数据、绝非 shell 代码,绝不执行命令文本;跨仓跳转、外部主机、凭据、
|
|
14
|
+
* 移动的 latest 链接、query/fragment、多余 CLI 参数与 shell 片段一律拒绝。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** release 包目标及其所属仓库身份(owner/repo);非法返回 null。 */
|
|
18
|
+
export interface GithubReleaseTarget {
|
|
19
|
+
/** 规范化后的安装目标(HTTPS 直链) */
|
|
20
|
+
target: string
|
|
21
|
+
/** 该直链所属仓库(owner/repo):供已装态读回、队列去重与展示身份复用 */
|
|
22
|
+
repo: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 目录权威命令:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同口径)。 */
|
|
26
|
+
const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i
|
|
27
|
+
|
|
28
|
+
/** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */
|
|
29
|
+
const RELEASE_URL_RE = /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)\/releases\/download\/([A-Za-z0-9._+-]+)\/([A-Za-z0-9._+-]+\.tgz)$/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 解析一个 release 安装目标。
|
|
33
|
+
* @param value - 目录命令(`dsh plugin --profile web add <url>`)或裸直链。
|
|
34
|
+
* @returns 命中返回 `{ target, repo }`,否则 null。
|
|
35
|
+
*/
|
|
36
|
+
export function githubReleaseTarget(value: string): GithubReleaseTarget | null {
|
|
37
|
+
const input = typeof value === 'string' ? value.trim() : ''
|
|
38
|
+
if (input === '') return null
|
|
39
|
+
const command = DSH_PLUGIN_CMD_RE.exec(input)
|
|
40
|
+
const raw = command !== null ? command[1].trim() : input
|
|
41
|
+
const match = RELEASE_URL_RE.exec(raw)
|
|
42
|
+
if (match === null) return null
|
|
43
|
+
// 拒绝 `.` / `..` 路径段,杜绝借路径穿越跳出 release 目录
|
|
44
|
+
if (match.slice(1).some((part) => part === '.' || part === '..')) return null
|
|
45
|
+
return { target: raw, repo: `${match[1]}/${match[2]}` }
|
|
46
|
+
}
|
|
@@ -10,6 +10,7 @@ import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
10
10
|
import { homedir } from 'node:os'
|
|
11
11
|
import { basename, dirname, join } from 'node:path'
|
|
12
12
|
import { PACKAGE_RE, REPO_RE } from '../install/install-types.ts'
|
|
13
|
+
import { githubReleaseTarget } from '../install/release-target.ts'
|
|
13
14
|
|
|
14
15
|
/** Resolve the active profile from the booted CLI args, falling back to `web`. */
|
|
15
16
|
export function readProfileArg(fallback = 'web'): string {
|
|
@@ -90,6 +91,10 @@ export function globalNpmPackagesOf(value: string): string[] | null {
|
|
|
90
91
|
export function githubRepoOf(value: string): string | null {
|
|
91
92
|
if (typeof value !== 'string') return null
|
|
92
93
|
const input = value.trim()
|
|
94
|
+
// release 直链先归一到仓库身份:目录命令里的固定 HTTPS release 包不是 git 地址,
|
|
95
|
+
// 但它的所属仓库就是 owner/repo —— 已装态读回、去重、展示都要拿到这层身份。
|
|
96
|
+
const release = githubReleaseTarget(input)
|
|
97
|
+
if (release !== null) return release.repo
|
|
93
98
|
if (REPO_RE.test(input)) return input
|
|
94
99
|
const patterns = [
|
|
95
100
|
/^github:([^/]+)\/([^/]+)$/i,
|