@noob-stupid/dsh-plugin-console 0.3.67 → 0.4.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/lib/client.js +126 -31
- package/lib/index.js +60 -9687
- package/lib/server/domain/ai-run.js +479 -0
- package/lib/server/domain/ai.js +246 -0
- package/lib/server/domain/compat.js +474 -0
- package/lib/server/domain/components.js +108 -0
- package/lib/server/domain/dep-source.js +122 -0
- package/lib/server/domain/format-contract.js +265 -0
- package/lib/server/domain/format-scan.js +431 -0
- package/lib/server/domain/framework.js +393 -0
- package/lib/server/domain/install-job.js +561 -0
- package/lib/server/domain/install.js +599 -0
- package/lib/server/domain/jobs.js +28 -0
- package/lib/server/domain/market.js +409 -0
- package/lib/server/domain/patch.js +203 -0
- package/lib/server/domain/presets.js +93 -0
- package/lib/server/domain/quarantine.js +224 -0
- package/lib/server/domain/release-source.js +504 -0
- package/lib/server/domain/repoland.js +119 -0
- package/lib/server/domain/revoke.js +184 -0
- package/lib/server/domain/runtime.js +118 -0
- package/lib/server/domain/selfupdate.js +319 -0
- package/lib/server/domain/skills.js +234 -0
- package/lib/server/domain/sources.js +297 -0
- package/lib/server/domain/suite.js +220 -0
- package/lib/server/infra/exec.js +98 -0
- package/lib/server/infra/fsx.js +163 -0
- package/lib/server/infra/fw-integrity-check.js +37 -0
- package/lib/server/infra/http.js +373 -0
- package/lib/server/infra/httpd.js +51 -0
- package/lib/server/infra/mask.js +19 -0
- package/lib/server/infra/paths.js +177 -0
- package/lib/server/infra/semver.js +168 -0
- package/lib/server/routes/ai.js +172 -0
- package/lib/server/routes/components.js +254 -0
- package/lib/server/routes/framework-preflight.js +154 -0
- package/lib/server/routes/framework-upgrade.js +679 -0
- package/lib/server/routes/framework.js +544 -0
- package/lib/server/routes/github-login.js +198 -0
- package/lib/server/routes/index.js +128 -0
- package/lib/server/routes/install.js +116 -0
- package/lib/server/routes/market.js +415 -0
- package/lib/server/routes/plugins.js +562 -0
- package/lib/server/routes/skills.js +107 -0
- package/lib/server/routes/sources.js +437 -0
- package/lib/server/routes/state.js +125 -0
- package/lib/server/state.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// L2 · routes —— 市场(/search · /enrich · /repo · /subpackages · /market-index)
|
|
2
|
+
// 分层 Step 8b:从 lib/index.js 的 handle() 原样搬出(只搬移未改逻辑;缩进保持原样)
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { writeFile } from 'node:fs/promises'
|
|
6
|
+
import { resolve } from 'node:path'
|
|
7
|
+
import { readGithubAuth } from '../domain/install.js'
|
|
8
|
+
import { enrichItems, fetchRepoPackage, fetchSubpackageNames, githubRepoInfo, hasDirectNameHit, normalizePlatformItems, searchNpmPackages, searchSubpackageItems } from '../domain/market.js'
|
|
9
|
+
import { SKILL_TOPICS, detectSkillRepo, fetchSkillMeta } from '../domain/skills.js'
|
|
10
|
+
import { DEFAULT_SEARCH, gitCloneUrls, orderedRegistries, readGiteeConfig, readSources } from '../domain/sources.js'
|
|
11
|
+
import { GITHUB_API, META_BUDGET_MS, curlJson, fetchJsonUrl, githubJson, looksLikeGitmodules, rawTextWithFallback } from '../infra/http.js'
|
|
12
|
+
import { sendError, sendJson } from '../infra/httpd.js'
|
|
13
|
+
import { marketIndexCacheFile } from '../infra/paths.js'
|
|
14
|
+
import { marketIndexCache, setMarketIndexCache } from '../state.js'
|
|
15
|
+
|
|
16
|
+
async function routeSearch(req, res, rc) {
|
|
17
|
+
const DEFAULT_SEARCH = rc.deps.DEFAULT_SEARCH
|
|
18
|
+
const ctx = rc.ctx
|
|
19
|
+
const pathname = rc.pathname
|
|
20
|
+
const method = rc.method
|
|
21
|
+
const body = rc.body
|
|
22
|
+
const raw = typeof body.q === 'string' ? body.q.trim() : ''
|
|
23
|
+
const query = raw === '' ? DEFAULT_SEARCH : raw
|
|
24
|
+
const page = Math.max(Math.min(Number.parseInt(String(body.page), 10) || 1, 5), 1)
|
|
25
|
+
const source = typeof body.source === 'string' && body.source !== '' ? body.source : 'github'
|
|
26
|
+
const all = body.all === true
|
|
27
|
+
const auth = readGithubAuth()
|
|
28
|
+
let items = []
|
|
29
|
+
// 给前端的补充说明(如"代码搜索需登录")——放响应里,前端据此给提示,不改 items 结构
|
|
30
|
+
const extraNotes = {}
|
|
31
|
+
// 增量检索(前端浏览器直连成功时**并行**调用):只补「npm 包名映射 + in:readme 重查 + 代码搜索子包」,
|
|
32
|
+
// 不重复仓库搜索。为什么需要它:直连成功就不会走本路由,而未登录用户恰恰只能走直连
|
|
33
|
+
// (2026-09-20 事故:未登录 + 索引加载失败 → 三条检索路全断,搜 web-all 搜不到 dsh-web)。
|
|
34
|
+
if (body.extras === true) {
|
|
35
|
+
const extra = []
|
|
36
|
+
if (raw !== '') {
|
|
37
|
+
try { extra.push(...await searchNpmPackages(raw, orderedRegistries(readSources()), 3, auth.token)) } catch {}
|
|
38
|
+
try {
|
|
39
|
+
const again = await githubJson(
|
|
40
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(`${raw} in:name,description,readme${all ? '' : ' topic:dsh-plugin'}`)}&sort=stars&order=desc&per_page=20&page=1`,
|
|
41
|
+
req.signal,
|
|
42
|
+
auth.token,
|
|
43
|
+
)
|
|
44
|
+
extra.push(...normalizePlatformItems(again.items ?? [], 'main').map((it) => ({ ...it, source: 'github', viaReadme: true })))
|
|
45
|
+
} catch {}
|
|
46
|
+
try { extra.push(...await searchSubpackageItems(raw, auth.token, req.signal)) } catch {}
|
|
47
|
+
}
|
|
48
|
+
sendJson(res, 200, {
|
|
49
|
+
ok: true,
|
|
50
|
+
query: raw,
|
|
51
|
+
items: extra,
|
|
52
|
+
extras: true,
|
|
53
|
+
authenticated: auth.loggedIn,
|
|
54
|
+
source: 'github',
|
|
55
|
+
...(raw !== '' && !auth.loggedIn ? { codeSearchSkipped: true } : {}),
|
|
56
|
+
})
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (body.multi === true) {
|
|
60
|
+
// 多源汇总:GitHub + 全部自定义搜索源并行检索,结果合并(每项带 source 标记)。
|
|
61
|
+
// Gitee 为直装模式(关键词搜索无意义),不参与多源汇总。
|
|
62
|
+
const sources = readSources()
|
|
63
|
+
const tasks = [
|
|
64
|
+
(async () => {
|
|
65
|
+
try {
|
|
66
|
+
const data = await githubJson(
|
|
67
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(all ? query : `${query} topic:dsh-plugin`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
68
|
+
req.signal,
|
|
69
|
+
auth.token,
|
|
70
|
+
)
|
|
71
|
+
return normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github', sourceName: 'GitHub' }))
|
|
72
|
+
} catch {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
})(),
|
|
76
|
+
...sources.searchSources.filter((s) => s.type === 'custom').map((s) => (async () => {
|
|
77
|
+
try {
|
|
78
|
+
const url = s.url.replace('{q}', encodeURIComponent(query)).replace('{page}', String(page))
|
|
79
|
+
const data = await fetchJsonUrl(url, 15000, s.headers ?? {})
|
|
80
|
+
return normalizePlatformItems(data, 'main').map((item) => ({ ...item, source: s.id, sourceName: s.name }))
|
|
81
|
+
} catch {
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
})()),
|
|
85
|
+
]
|
|
86
|
+
const results = await Promise.all(tasks)
|
|
87
|
+
items = results.flat()
|
|
88
|
+
items = await enrichItems(items)
|
|
89
|
+
sendJson(res, 200, { ok: true, query, items, authenticated: auth.loggedIn, source: 'all', multi: true })
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if (body.skills === true) {
|
|
93
|
+
// 技能模式搜索:agent-skills / claude-skills / dsh-skill 三 topic 并行检索后合并去重
|
|
94
|
+
// (GitHub search 的 OR 语法优先级不可靠,分开查最稳),按 star 排序取前 20。
|
|
95
|
+
if (source !== 'github') {
|
|
96
|
+
sendError(res, 400, '技能搜索仅支持 GitHub 源')
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
const keyword = raw === '' ? '' : `${raw} in:name,description,topics `
|
|
100
|
+
const tasks = SKILL_TOPICS.map((topic) => (async () => {
|
|
101
|
+
try {
|
|
102
|
+
const data = await githubJson(
|
|
103
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(`${keyword}topic:${topic}`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
104
|
+
req.signal,
|
|
105
|
+
auth.token,
|
|
106
|
+
)
|
|
107
|
+
return normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github', skillTopics: [topic] }))
|
|
108
|
+
} catch {
|
|
109
|
+
return []
|
|
110
|
+
}
|
|
111
|
+
})())
|
|
112
|
+
const merged = (await Promise.all(tasks)).flat()
|
|
113
|
+
const seen = new Set()
|
|
114
|
+
items = []
|
|
115
|
+
for (const item of merged.sort((a, b) => b.stars - a.stars)) {
|
|
116
|
+
if (seen.has(item.fullName)) continue
|
|
117
|
+
seen.add(item.fullName)
|
|
118
|
+
items.push(item)
|
|
119
|
+
if (items.length >= 20) break
|
|
120
|
+
}
|
|
121
|
+
items = await enrichItems(items)
|
|
122
|
+
sendJson(res, 200, { ok: true, query, items, authenticated: auth.loggedIn, source: 'github', skills: true })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if (source === 'gitee') {
|
|
126
|
+
// Gitee 官方 v5 搜索接口(search/repositories)已废弃(恒返回空);
|
|
127
|
+
// so.gitee.com/v1(Indexea 后端)有百度云 WAF 反爬且需映答账号 token。
|
|
128
|
+
// 因此 Gitee 源采用仓库直装模式:输入 owner/repo 直接取仓库信息(公开接口,无需登录)。
|
|
129
|
+
const gitee = readGiteeConfig(readSources())
|
|
130
|
+
let repo = ''
|
|
131
|
+
let giteeError = ''
|
|
132
|
+
try {
|
|
133
|
+
repo = githubRepoInfo(query)
|
|
134
|
+
} catch (error) {
|
|
135
|
+
giteeError = error instanceof Error ? error.message : String(error)
|
|
136
|
+
}
|
|
137
|
+
if (repo) {
|
|
138
|
+
try {
|
|
139
|
+
const tokenQ = gitee.token ? `?access_token=${encodeURIComponent(gitee.token)}` : ''
|
|
140
|
+
// repo 已由 githubRepoInfo 校验;分段编码(只编码中文等非 ASCII,斜杠保留原样——
|
|
141
|
+
// Gitee 服务器不认 %2F 编码的路径分隔,返回 404)
|
|
142
|
+
const [owner, name] = repo.split('/')
|
|
143
|
+
const data = await fetchJsonUrl(`https://gitee.com/api/v5/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}${tokenQ}`)
|
|
144
|
+
items = normalizePlatformItems([data], 'master').map((item) => ({ ...item, source: 'gitee' }))
|
|
145
|
+
} catch (error) {
|
|
146
|
+
giteeError = error instanceof Error ? error.message : String(error)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
sendJson(res, 200, { ok: true, query, items, authenticated: auth.loggedIn, source, giteeNeedsLogin: false, directOnly: true, giteeError })
|
|
150
|
+
return
|
|
151
|
+
} else if (source !== 'github') {
|
|
152
|
+
// 自定义搜索源:URL 模板({q}/{page} 占位符),返回数组或 {items} 结构;支持配置的请求头
|
|
153
|
+
const sources = readSources()
|
|
154
|
+
const custom = sources.searchSources.find((s) => s.id === source && s.type === 'custom')
|
|
155
|
+
if (!custom) {
|
|
156
|
+
sendError(res, 404, `没有这个搜索源:${source}`)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const url = custom.url
|
|
160
|
+
.replace('{q}', encodeURIComponent(query))
|
|
161
|
+
.replace('{page}', String(page))
|
|
162
|
+
const data = await fetchJsonUrl(url, 15000, custom.headers ?? {})
|
|
163
|
+
items = normalizePlatformItems(data, 'main').map((item) => ({ ...item, source, sourceName: custom.name }))
|
|
164
|
+
} else {
|
|
165
|
+
// npm 包名搜索与 GitHub 仓库搜索**并行**:npm 侧要查 registry 搜索 + packument + 仓库元数据
|
|
166
|
+
// (实测 4.5~9s),串行会把两段等待叠加到用户身上。
|
|
167
|
+
const npmPromise = raw !== '' && !body.skills
|
|
168
|
+
? searchNpmPackages(raw, orderedRegistries(readSources()), 3, auth.token).catch(() => [])
|
|
169
|
+
: Promise.resolve([])
|
|
170
|
+
const data = await githubJson(
|
|
171
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(all ? query : `${query} topic:dsh-plugin`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
172
|
+
req.signal,
|
|
173
|
+
auth.token,
|
|
174
|
+
)
|
|
175
|
+
items = normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github' }))
|
|
176
|
+
items = await enrichItems(items)
|
|
177
|
+
// monorepo 子包增强(OpenViking/examples/dsh-memory-plugin 等可按子包名搜到;
|
|
178
|
+
// 代码搜索需登录,未登录时该函数返回空数组,见 domain/market.js 说明)
|
|
179
|
+
if (raw !== '' && !body.skills) {
|
|
180
|
+
for (const sub of await searchSubpackageItems(raw, auth.token, req.signal)) {
|
|
181
|
+
if (!items.some((x) => x.fullName === sub.fullName)) items.push(sub)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ── B′:README 重查(2026-09-20)────────────────────────────────────────
|
|
185
|
+
// 仓库搜索只在「仓库名 + 描述 + topics」里找词,所以只写在 README 或仓库文件里的名字搜不到。
|
|
186
|
+
// 典型:`web-all` 只是 npm 包名 + `packages/dsh-web-all/package.json` 的内容,
|
|
187
|
+
// `q=web-all topic:dsh-plugin` 32 条里没有 dsh-web;而 `web-all in:readme` 第 9 条就是它。
|
|
188
|
+
// 首屏没有"名字逐词命中"的条目时,用 in:name,description,readme 再查一次(未登录也能用)。
|
|
189
|
+
if (raw !== '' && !body.skills && page === 1 && !hasDirectNameHit(items, raw)) {
|
|
190
|
+
try {
|
|
191
|
+
const again = await githubJson(
|
|
192
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(`${raw} in:name,description,readme${all ? '' : ' topic:dsh-plugin'}`)}&sort=stars&order=desc&per_page=20&page=1`,
|
|
193
|
+
req.signal,
|
|
194
|
+
auth.token,
|
|
195
|
+
)
|
|
196
|
+
for (const it of normalizePlatformItems(again.items ?? [], 'main')) {
|
|
197
|
+
if (items.some((x) => x.fullName === it.fullName)) continue
|
|
198
|
+
items.push({ ...it, source: 'github', viaReadme: true })
|
|
199
|
+
if (items.length >= 40) break
|
|
200
|
+
}
|
|
201
|
+
} catch {}
|
|
202
|
+
}
|
|
203
|
+
// ── A:npm 包名搜索(2026-09-20)────────────────────────────────────────
|
|
204
|
+
// 用户输入常常是 npm 包名(`web-all`),而 GitHub 元数据里没有它 → 走 registry 搜索接口反查
|
|
205
|
+
// 包 → repository.url → 仓库,命中**置顶**并带 npmPackage 标记(前端按包名安装)。
|
|
206
|
+
// 不依赖静态索引、不依赖 GitHub 登录;registry 走配置的软件源(默认国内镜像)。
|
|
207
|
+
// 已在列表里的同仓库条目(例如代码搜索加进来的"子包")合并 npm 信息后上移——精确命中不该排在第 21 位。
|
|
208
|
+
if (raw !== '' && !body.skills) {
|
|
209
|
+
for (const it of (await npmPromise).reverse()) {
|
|
210
|
+
const existingIdx = items.findIndex((x) => x.fullName === it.fullName)
|
|
211
|
+
const existing = existingIdx >= 0 ? items.splice(existingIdx, 1)[0] : null
|
|
212
|
+
items.unshift({ ...(existing ?? {}), ...it })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// 代码搜索(monorepo 子包)需要 GitHub 登录:未登录时它拿不到结果,
|
|
216
|
+
// 前端据此提示"登录后可按子包名搜索"(2026-09-20 事故复盘:未登录用户三条检索路径全断)
|
|
217
|
+
if (raw !== '' && !body.skills && !auth.loggedIn) extraNotes.codeSearchSkipped = true
|
|
218
|
+
}
|
|
219
|
+
items = items.filter((item) => item.fullName !== '')
|
|
220
|
+
sendJson(res, 200, { ok: true, query, items, authenticated: auth.loggedIn, source, ...(extraNotes) })
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function routeEnrich(req, res, rc) {
|
|
225
|
+
const ctx = rc.ctx
|
|
226
|
+
const url = rc.url
|
|
227
|
+
const pathname = rc.pathname
|
|
228
|
+
const method = rc.method
|
|
229
|
+
const body = rc.body
|
|
230
|
+
// 为浏览器直连的搜索结果补官方/聚合标记(服务端通道可靠;客户端直连无标记能力)
|
|
231
|
+
const raw = Array.isArray(body.items) ? body.items.slice(0, 30) : []
|
|
232
|
+
const items = await enrichItems(raw)
|
|
233
|
+
sendJson(res, 200, { ok: true, items })
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function routeRepo(req, res, rc) {
|
|
238
|
+
const ctx = rc.ctx
|
|
239
|
+
const url = rc.url
|
|
240
|
+
const pathname = rc.pathname
|
|
241
|
+
const method = rc.method
|
|
242
|
+
const body = rc.body
|
|
243
|
+
const repo = githubRepoInfo(typeof body.repo === 'string' ? body.repo : '')
|
|
244
|
+
const auth = readGithubAuth()
|
|
245
|
+
// meta 降级策略:githubJson(https+镜像+gh)与 curl 竞速,8 秒超时即降级——
|
|
246
|
+
// Promise.any 全失败时要等最慢分支(黑洞期 https 41.5s),加 race 超时避免拖累整体。
|
|
247
|
+
// 8s 而非旧值 3s:IPv6 无路由的环境里单条通道就要 5.4s(2026-09-20 实测),3s 必输 → branch 取错。
|
|
248
|
+
let meta = null
|
|
249
|
+
try {
|
|
250
|
+
meta = await Promise.race([
|
|
251
|
+
Promise.any([
|
|
252
|
+
githubJson(`${GITHUB_API}/repos/${repo}`, req.signal, auth.token),
|
|
253
|
+
curlJson(`${GITHUB_API}/repos/${repo}`, 12000, {}, { ipv4: true }),
|
|
254
|
+
]),
|
|
255
|
+
new Promise((resolve) => setTimeout(() => resolve(null), META_BUDGET_MS)),
|
|
256
|
+
])
|
|
257
|
+
} catch {}
|
|
258
|
+
const branch = meta?.default_branch ?? 'main'
|
|
259
|
+
const pkg = await fetchRepoPackage(repo, branch)
|
|
260
|
+
const skill = await detectSkillRepo(repo, branch)
|
|
261
|
+
const skillMeta = skill.hasSkill ? await fetchSkillMeta(repo, branch, skill.skillDir) : null
|
|
262
|
+
// 套装识别:根 .gitmodules 存在**且内容真的是 gitmodules**(只判非 null 会被代理/CDN 对不存在
|
|
263
|
+
// 文件回的 2xx 空 body 骗到,把普通插件标成套装——2026-09-19 用户反馈事故)
|
|
264
|
+
const hasSuite = looksLikeGitmodules(await rawTextWithFallback(repo, branch, '.gitmodules'))
|
|
265
|
+
// 官方安装方式(详情面板展示 + 一键复制,供用户手动安装):
|
|
266
|
+
// 套装 → 仓库 install.ps1/README 的官方步骤;普通/聚合 → dsh plugin add 官方命令
|
|
267
|
+
let installCommand = null
|
|
268
|
+
if (hasSuite) {
|
|
269
|
+
const short = repo.split('/')[1] ?? repo
|
|
270
|
+
const hasInstallScript = (await rawTextWithFallback(repo, branch, 'install.ps1')) !== null
|
|
271
|
+
|| (await rawTextWithFallback(repo, branch, 'install.sh')) !== null
|
|
272
|
+
// 纯命令(无注释,CMD/PowerShell 通用);不再关闭 TLS 校验;
|
|
273
|
+
// 脚本用 powershell -File 调用,CMD 里也能跑
|
|
274
|
+
installCommand = [
|
|
275
|
+
`git clone --recurse-submodules ${gitCloneUrls(repo)[0]}`,
|
|
276
|
+
`cd ${short}`,
|
|
277
|
+
hasInstallScript
|
|
278
|
+
? `powershell -ExecutionPolicy Bypass -File install.ps1`
|
|
279
|
+
: `git submodule update --init --recursive`,
|
|
280
|
+
].join('\n')
|
|
281
|
+
} else {
|
|
282
|
+
installCommand = `dsh plugin --profile web add github:${repo}`
|
|
283
|
+
}
|
|
284
|
+
sendJson(res, 200, {
|
|
285
|
+
ok: true,
|
|
286
|
+
repo,
|
|
287
|
+
defaultBranch: branch,
|
|
288
|
+
description: meta?.description ?? '',
|
|
289
|
+
stars: meta?.stargazers_count ?? 0,
|
|
290
|
+
packageName: pkg?.name ?? null,
|
|
291
|
+
packageDescription: pkg?.description ?? null,
|
|
292
|
+
hasPackageJson: pkg !== null,
|
|
293
|
+
privateRoot: pkg !== null && pkg.private === true,
|
|
294
|
+
hasSkill: skill.hasSkill,
|
|
295
|
+
skillDir: skill.skillDir,
|
|
296
|
+
skill: skillMeta,
|
|
297
|
+
hasSuite,
|
|
298
|
+
installCommand,
|
|
299
|
+
dshHint: pkg !== null && (
|
|
300
|
+
typeof pkg.name === 'string' && /(^|-)dsh[-/]/u.test(pkg.name)
|
|
301
|
+
|| pkg.peerDependencies?.['@deepseek-ai/cordis'] !== undefined
|
|
302
|
+
|| Array.isArray(pkg.keywords) && pkg.keywords.includes('dsh-plugin')
|
|
303
|
+
),
|
|
304
|
+
})
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function routeSubpackages(req, res, rc) {
|
|
309
|
+
const ctx = rc.ctx
|
|
310
|
+
const url = rc.url
|
|
311
|
+
const pathname = rc.pathname
|
|
312
|
+
const method = rc.method
|
|
313
|
+
const body = rc.body
|
|
314
|
+
const repo = githubRepoInfo(typeof body.repo === 'string' ? body.repo : '')
|
|
315
|
+
const branch = typeof body.branch === 'string' && body.branch ? body.branch : 'main'
|
|
316
|
+
const auth = readGithubAuth()
|
|
317
|
+
// 复用安装链的防护实现:任一 raw 拉取失败只跳过该子包,不整体 500
|
|
318
|
+
const subpackages = await fetchSubpackageNames(repo, branch, auth.token)
|
|
319
|
+
sendJson(res, 200, { ok: true, repo, branch, subpackages })
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function routeMarketIndex(req, res, rc) {
|
|
324
|
+
const ctx = rc.ctx
|
|
325
|
+
const url = rc.url
|
|
326
|
+
const pathname = rc.pathname
|
|
327
|
+
const method = rc.method
|
|
328
|
+
const body = rc.body
|
|
329
|
+
// 静态插件索引:按「软件源 → 索引源」主→备顺序拉取 + 10 分钟内存缓存(市场秒开、零 GitHub API 调用)。
|
|
330
|
+
// 全部索引源失败时回退落盘缓存(内网/断网仍可浏览,响应带 offline 标记),无缓存则区分错误类型。
|
|
331
|
+
if (marketIndexCache !== null && Date.now() - marketIndexCache.at < 600000) {
|
|
332
|
+
sendJson(res, 200, { ok: true, sourceName: marketIndexCache.sourceName ?? null, ...marketIndexCache.data })
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
const indexConf = readSources()
|
|
336
|
+
let indexList = [...(indexConf.indexSources ?? [])].sort((a, b) => (b.primary === true ? 1 : 0) - (a.primary === true ? 1 : 0))
|
|
337
|
+
// 合并模式:并发拉取所有索引源并去重合并(公共索引 + 内网私有索引同时可见)
|
|
338
|
+
if (indexConf.indexMerge === true && indexList.length > 1) {
|
|
339
|
+
const fetched = await Promise.all(indexList.map(async (src) => {
|
|
340
|
+
try {
|
|
341
|
+
// 各源独立短超时:单个慢源(被墙镜像/不可达内网)不该拖垮整体
|
|
342
|
+
const data = await fetchJsonUrl(src.url, 8000)
|
|
343
|
+
return data && Array.isArray(data.items) ? { src, data } : null
|
|
344
|
+
} catch { return null }
|
|
345
|
+
}))
|
|
346
|
+
const good = fetched.filter((x) => x !== null)
|
|
347
|
+
if (good.length > 0) {
|
|
348
|
+
const seen = new Set()
|
|
349
|
+
const skillSeen = new Set()
|
|
350
|
+
const items = []
|
|
351
|
+
const skills = []
|
|
352
|
+
for (const { data } of good) {
|
|
353
|
+
for (const it of data.items) {
|
|
354
|
+
const key = typeof it?.fullName === 'string' ? it.fullName : JSON.stringify(it)
|
|
355
|
+
if (seen.has(key)) continue
|
|
356
|
+
seen.add(key)
|
|
357
|
+
items.push(it)
|
|
358
|
+
}
|
|
359
|
+
for (const sk of (Array.isArray(data.skills) ? data.skills : [])) {
|
|
360
|
+
const key = typeof sk?.fullName === 'string' ? sk.fullName : JSON.stringify(sk)
|
|
361
|
+
if (skillSeen.has(key)) continue
|
|
362
|
+
skillSeen.add(key)
|
|
363
|
+
skills.push(sk)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const sourceName = good.map((g) => g.src.name).join(' + ')
|
|
367
|
+
const merged = { items, skills, skillCount: skills.length, merged: true, sourceName }
|
|
368
|
+
setMarketIndexCache({ at: Date.now(), data: merged, sourceName })
|
|
369
|
+
try { await writeFile(marketIndexCacheFile(), JSON.stringify({ at: Date.now(), data: merged, sourceName }), 'utf8') } catch {}
|
|
370
|
+
sendJson(res, 200, { ok: true, ...merged })
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
// 所有源都失败 → 跳过逐个重试,直接进入下方缓存兜底
|
|
374
|
+
indexList = []
|
|
375
|
+
}
|
|
376
|
+
let lastError = null
|
|
377
|
+
let formatError = null
|
|
378
|
+
// 总预算:索引源扩容到 5 个后必须封顶,否则用户只会看到"市场一直转圈"。
|
|
379
|
+
// 实测(2026-09-20):fetchJsonUrl 内部是「curl 一次 + node:https 兜底(默认 20s 超时)」,
|
|
380
|
+
// 单个源最坏要 ~28s,5 个源曾实测到 **65s** 才回退到落盘缓存。
|
|
381
|
+
// 这里改为**每源单次 curl**(curlJson,8s 硬超时)+ 整体 12s 预算 → 最坏 ≈ 20s,常见 <1s。
|
|
382
|
+
const deadline = Date.now() + 12000
|
|
383
|
+
for (const src of indexList) {
|
|
384
|
+
if (Date.now() > deadline) break
|
|
385
|
+
try {
|
|
386
|
+
const data = await curlJson(src.url, 8000)
|
|
387
|
+
if (data && Array.isArray(data.items)) {
|
|
388
|
+
setMarketIndexCache({ at: Date.now(), data, sourceName: src.name })
|
|
389
|
+
try { await writeFile(marketIndexCacheFile(), JSON.stringify({ at: Date.now(), data, sourceName: src.name }), 'utf8') } catch {}
|
|
390
|
+
sendJson(res, 200, { ok: true, sourceName: src.name, ...data })
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
formatError = `索引格式异常(${src.name} 未返回 items 数组)`
|
|
394
|
+
} catch (error) {
|
|
395
|
+
lastError = error
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
const cached = JSON.parse(readFileSync(marketIndexCacheFile(), 'utf8'))
|
|
400
|
+
if (cached && cached.data && Array.isArray(cached.data.items)) {
|
|
401
|
+
setMarketIndexCache({ at: Date.now(), data: cached.data, sourceName: cached.sourceName ?? null })
|
|
402
|
+
sendJson(res, 200, { ok: true, offline: true, cachedAt: typeof cached.at === 'number' ? cached.at : null, sourceName: cached.sourceName ?? null, ...cached.data })
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
} catch {}
|
|
406
|
+
const reason = formatError !== null
|
|
407
|
+
? formatError
|
|
408
|
+
: `网络不可达(${indexList.length} 个索引源全部失败):${lastError?.message ?? '未知错误'}`
|
|
409
|
+
// 说清后果(2026-09-20 另一位用户实测的困惑):索引不在时市场只剩 GitHub 实时结果,
|
|
410
|
+
// 收录条目与本地索引模糊匹配一起失效(他搜 web-all 搜不到 dsh-web 全家桶就是这个原因)
|
|
411
|
+
sendError(res, 500, `索引加载失败:${reason}(此时市场只能搜 GitHub 实时结果,收录条目可能看不到;可在「软件源 → 索引源」增删/更换索引源后重试)`)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export { routeSearch, routeEnrich, routeRepo, routeSubpackages, routeMarketIndex }
|