@noob-stupid/dsh-plugin-console 0.5.33 → 0.5.35
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 +516 -220
- package/lib/server/domain/compat-verdict.js +241 -0
- package/lib/server/domain/compat.js +84 -16
- package/lib/server/domain/framework-cleanup.js +225 -24
- package/lib/server/domain/framework-residuals.js +52 -3
- package/lib/server/domain/market-search.js +242 -0
- package/lib/server/domain/settings-api-scan.js +16 -1
- package/lib/server/routes/market.js +108 -35
- package/lib/server/routes/plugins.js +24 -13
- package/lib/server/routes/state.js +14 -3
- package/package.json +1 -1
|
@@ -34,6 +34,21 @@ import { join } from 'node:path'
|
|
|
34
34
|
/** 0.1.2-rc.1 起被官方删除的 dsh-settings API(命中即"实际不兼容")。 */
|
|
35
35
|
const REMOVED_SETTINGS_SYMBOLS = ['settingsNamespace', 'installSettingsSection']
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* **判据版本戳**(0.5.34 加法):判据一变就必须改这个字符串。
|
|
39
|
+
*
|
|
40
|
+
* 为什么需要它(真机背景 2026-10-01):`compat-pending.json` 里的结论是"当时的判据"写下的,
|
|
41
|
+
* 但面板把它当**事实**一直展示:`dshmarket` 记录里压着
|
|
42
|
+
* `check:"fail"` + `checkNote:"源码仍引用 0.1.2 起已删除的 dsh-settings API…"` ——
|
|
43
|
+
* 那是 0.5.33 收紧判据**之前**的误报结论(真机复核:现在 0 命中),
|
|
44
|
+
* 于是面板持续显示"实际不兼容,启用会让整个服务启动崩溃",把用户永久钉在「待适配」。
|
|
45
|
+
* 有了版本戳,"这条结论是哪一版判据算的"就可以被后续读取代发判读 → 过期即重算(见 compat-verdict.js)。
|
|
46
|
+
*
|
|
47
|
+
* v1 = 0.3.35 的「标识符边界 + 排除局部定义」(挡不住注释里的名字与对象键上的名字)
|
|
48
|
+
* v2 = 0.5.33 的「真的从 `@deepseek-ai/dsh-settings` 绑定进来」(本文件实现)
|
|
49
|
+
*/
|
|
50
|
+
const SCAN_CRITERIA_VERSION = 'dsh-settings-scan/2'
|
|
51
|
+
|
|
37
52
|
/** 这些 API 原本来自哪个包(只有真的从它绑定进来的名字才算引用)。 */
|
|
38
53
|
const SETTINGS_MODULE = '@deepseek-ai/dsh-settings'
|
|
39
54
|
|
|
@@ -248,4 +263,4 @@ function scanSettingsApiUsage(pkgDir) {
|
|
|
248
263
|
return found
|
|
249
264
|
}
|
|
250
265
|
|
|
251
|
-
export { REMOVED_SETTINGS_SYMBOLS, SETTINGS_MODULE, maskLiterals, settingsApiBindings, settingsApiReferences, referencesRemovedSymbol, scanSettingsApiUsage }
|
|
266
|
+
export { REMOVED_SETTINGS_SYMBOLS, SCAN_CRITERIA_VERSION, SETTINGS_MODULE, maskLiterals, settingsApiBindings, settingsApiReferences, referencesRemovedSymbol, scanSettingsApiUsage }
|
|
@@ -6,6 +6,7 @@ import { writeFile } from 'node:fs/promises'
|
|
|
6
6
|
import { resolve } from 'node:path'
|
|
7
7
|
import { readGithubAuth } from '../domain/install.js'
|
|
8
8
|
import { enrichItems, fetchRepoPackage, fetchSubpackageNames, githubRepoInfo, hasDirectNameHit, normalizePlatformItems, searchNpmPackages, searchSubpackageItems } from '../domain/market.js'
|
|
9
|
+
import { buildPageEnvelope, rankSearchResults } from '../domain/market-search.js'
|
|
9
10
|
import { SKILL_TOPICS, detectSkillRepo, fetchSkillMeta } from '../domain/skills.js'
|
|
10
11
|
import { DEFAULT_SEARCH, gitCloneUrls, orderedRegistries, readGiteeConfig, readSources } from '../domain/sources.js'
|
|
11
12
|
import { GITHUB_API, META_BUDGET_MS, curlJson, fetchJsonUrl, githubJson, looksLikeGitmodules, rawTextWithFallback } from '../infra/http.js'
|
|
@@ -13,19 +14,48 @@ import { sendError, sendJson } from '../infra/httpd.js'
|
|
|
13
14
|
import { marketIndexCacheFile } from '../infra/paths.js'
|
|
14
15
|
import { marketIndexCache, setMarketIndexCache } from '../state.js'
|
|
15
16
|
|
|
17
|
+
/** 技能检索的 per_page:三 topic 合并去重后不一定还有 20 条,取少了下拉就白丢条目
|
|
18
|
+
* (旧行为每 topic 20 条再截前 20 ⇒ 点「加载更多」也翻不出新东西)。 */
|
|
19
|
+
const SKILL_TOPIC_PER_PAGE = 50
|
|
20
|
+
/** 技能池缓存:同一个查询的**同一份合并池**重复切片 ⇒ 翻页幂等(不重不漏)。
|
|
21
|
+
* 不进 state.js:这是路由层的短命缓存(与静态索引缓存不是一回事)。 */
|
|
22
|
+
const SKILL_POOL_TTL_MS = 300000
|
|
23
|
+
const skillPoolCache = new Map()
|
|
24
|
+
function skillPoolCacheGet(key) {
|
|
25
|
+
const hit = skillPoolCache.get(key)
|
|
26
|
+
if (hit === undefined) return null
|
|
27
|
+
if (Date.now() - hit.at > SKILL_POOL_TTL_MS) { skillPoolCache.delete(key); return null }
|
|
28
|
+
return hit.items
|
|
29
|
+
}
|
|
30
|
+
function skillPoolCacheSet(key, items) {
|
|
31
|
+
if (skillPoolCache.size >= 8) skillPoolCache.delete(skillPoolCache.keys().next().value)
|
|
32
|
+
skillPoolCache.set(key, { at: Date.now(), items })
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
async function routeSearch(req, res, rc) {
|
|
17
36
|
const DEFAULT_SEARCH = rc.deps.DEFAULT_SEARCH
|
|
37
|
+
// 网络取数的注入缝(默认就是真通道):测试要断言"排序/分页/失败路径"时喂固定数据,
|
|
38
|
+
// 免得把判据与 CI 稳定性绑在网络可用性上(与 routes/index.js 的 deps 同一种风格)。
|
|
39
|
+
const fetchJson = typeof rc.deps?.fetchJson === 'function' ? rc.deps.fetchJson : fetchJsonUrl
|
|
40
|
+
const searchRepos = typeof rc.deps?.githubJson === 'function' ? rc.deps.githubJson : githubJson
|
|
18
41
|
const ctx = rc.ctx
|
|
19
42
|
const pathname = rc.pathname
|
|
20
43
|
const method = rc.method
|
|
21
44
|
const body = rc.body
|
|
22
45
|
const raw = typeof body.q === 'string' ? body.q.trim() : ''
|
|
23
46
|
const query = raw === '' ? DEFAULT_SEARCH : raw
|
|
24
|
-
|
|
47
|
+
// 0.5.35 改错:旧代码把 page **夹到 1..5** —— 第 6 次「加载更多」起原地返回第 5 页,
|
|
48
|
+
// 用户看到的是"怎么点都不出新条目"(与本次"没有加载更多"同族的第二个截断层)。
|
|
49
|
+
// 现在只做合法性下限(≥1),真实可翻页性由 buildPageEnvelope 依据数据源的 total 如实回报。
|
|
50
|
+
const page = Math.max(Number.parseInt(String(body.page), 10) || 1, 1)
|
|
25
51
|
const source = typeof body.source === 'string' && body.source !== '' ? body.source : 'github'
|
|
26
52
|
const all = body.all === true
|
|
27
53
|
const auth = readGithubAuth()
|
|
28
54
|
let items = []
|
|
55
|
+
// 数据源报的**总命中数**(GitHub search 的 total_count;自定义源没有就留 null —— 绝不编造)
|
|
56
|
+
let total = null
|
|
57
|
+
// in:readme 重查那一层有没有被自己的 40 条上限截断(说清楚,别让用户以为"就是没有")
|
|
58
|
+
let readmeTruncated = false
|
|
29
59
|
// 给前端的补充说明(如"代码搜索需登录")——放响应里,前端据此给提示,不改 items 结构
|
|
30
60
|
const extraNotes = {}
|
|
31
61
|
// 增量检索(前端浏览器直连成功时**并行**调用):只补「npm 包名映射 + in:readme 重查 + 代码搜索子包」,
|
|
@@ -36,7 +66,7 @@ async function routeSearch(req, res, rc) {
|
|
|
36
66
|
if (raw !== '') {
|
|
37
67
|
try { extra.push(...await searchNpmPackages(raw, orderedRegistries(readSources()), 3, auth.token)) } catch {}
|
|
38
68
|
try {
|
|
39
|
-
const again = await
|
|
69
|
+
const again = await searchRepos(
|
|
40
70
|
`${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
71
|
req.signal,
|
|
42
72
|
auth.token,
|
|
@@ -48,7 +78,8 @@ async function routeSearch(req, res, rc) {
|
|
|
48
78
|
sendJson(res, 200, {
|
|
49
79
|
ok: true,
|
|
50
80
|
query: raw,
|
|
51
|
-
|
|
81
|
+
// 增量结果同样过**唯一**排序函数:客户端拿到后只需合并,不需要(也不许)自己另排一份
|
|
82
|
+
items: rankSearchResults(extra, raw),
|
|
52
83
|
extras: true,
|
|
53
84
|
authenticated: auth.loggedIn,
|
|
54
85
|
source: 'github',
|
|
@@ -63,11 +94,12 @@ async function routeSearch(req, res, rc) {
|
|
|
63
94
|
const tasks = [
|
|
64
95
|
(async () => {
|
|
65
96
|
try {
|
|
66
|
-
const data = await
|
|
97
|
+
const data = await searchRepos(
|
|
67
98
|
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(all ? query : `${query} topic:dsh-plugin`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
68
99
|
req.signal,
|
|
69
100
|
auth.token,
|
|
70
101
|
)
|
|
102
|
+
if (typeof data?.total_count === 'number') total = data.total_count
|
|
71
103
|
return normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github', sourceName: 'GitHub' }))
|
|
72
104
|
} catch {
|
|
73
105
|
return []
|
|
@@ -76,7 +108,7 @@ async function routeSearch(req, res, rc) {
|
|
|
76
108
|
...sources.searchSources.filter((s) => s.type === 'custom').map((s) => (async () => {
|
|
77
109
|
try {
|
|
78
110
|
const url = s.url.replace('{q}', encodeURIComponent(query)).replace('{page}', String(page))
|
|
79
|
-
const data = await
|
|
111
|
+
const data = await fetchJson(url, 15000, s.headers ?? {})
|
|
80
112
|
return normalizePlatformItems(data, 'main').map((item) => ({ ...item, source: s.id, sourceName: s.name }))
|
|
81
113
|
} catch {
|
|
82
114
|
return []
|
|
@@ -86,40 +118,63 @@ async function routeSearch(req, res, rc) {
|
|
|
86
118
|
const results = await Promise.all(tasks)
|
|
87
119
|
items = results.flat()
|
|
88
120
|
items = await enrichItems(items)
|
|
89
|
-
|
|
121
|
+
// 多源合并后**只排一次**(唯一排序函数):跨源条目按同一套名字分层,同层才比 star
|
|
122
|
+
items = rankSearchResults(items, raw)
|
|
123
|
+
sendJson(res, 200, {
|
|
124
|
+
ok: true, query, items, authenticated: auth.loggedIn, source: 'all', multi: true,
|
|
125
|
+
...buildPageEnvelope({ shown: items.length, total, page }),
|
|
126
|
+
})
|
|
90
127
|
return
|
|
91
128
|
}
|
|
92
129
|
if (body.skills === true) {
|
|
93
|
-
// 技能模式搜索:agent-skills / claude-skills / dsh-skill 三 topic
|
|
94
|
-
// (GitHub search 的 OR
|
|
130
|
+
// 技能模式搜索:agent-skills / claude-skills / dsh-skill 三 topic 并行检索后合并去重。
|
|
131
|
+
// (GitHub search 的 OR 语法优先级不可靠,分开查最稳。)
|
|
132
|
+
// 0.5.35 改错:旧行为「取前 20 条」把合并池**截断**在 20 —— 客户端点「加载更多」请求下一页,
|
|
133
|
+
// 服务端仍从**每个 topic 的第 N 页**取 20 条再截前 20,去重后经常一条新的都没有(按钮白点)。
|
|
134
|
+
// 现在:合并池按唯一排序函数排序 + 去重,然后**照分页口径切片**;并集池按查询缓存,
|
|
135
|
+
// 同一查询的任何一页都从同一份池里切 ⇒ 翻页幂等(不重不漏)。
|
|
95
136
|
if (source !== 'github') {
|
|
96
137
|
sendError(res, 400, '技能搜索仅支持 GitHub 源')
|
|
97
138
|
return
|
|
98
139
|
}
|
|
99
140
|
const keyword = raw === '' ? '' : `${raw} in:name,description,topics `
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
141
|
+
const poolKey = `${raw}\u0000${all ? '1' : '0'}`
|
|
142
|
+
let pool = skillPoolCacheGet(poolKey)
|
|
143
|
+
if (pool === null) {
|
|
144
|
+
const tasks = SKILL_TOPICS.map((topic) => (async () => {
|
|
145
|
+
try {
|
|
146
|
+
const data = await searchRepos(
|
|
147
|
+
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(`${keyword}topic:${topic}`)}&sort=stars&order=desc&per_page=${SKILL_TOPIC_PER_PAGE}&page=1`,
|
|
148
|
+
req.signal,
|
|
149
|
+
auth.token,
|
|
150
|
+
)
|
|
151
|
+
if (typeof data?.total_count === 'number') total = (total ?? 0) + data.total_count
|
|
152
|
+
return normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github', skillTopics: [topic] }))
|
|
153
|
+
} catch {
|
|
154
|
+
return []
|
|
155
|
+
}
|
|
156
|
+
})())
|
|
157
|
+
const merged = (await Promise.all(tasks)).flat()
|
|
158
|
+
const seen = new Set()
|
|
159
|
+
const deduped = []
|
|
160
|
+
for (const item of rankSearchResults(merged, raw)) {
|
|
161
|
+
if (seen.has(item.fullName)) continue
|
|
162
|
+
seen.add(item.fullName)
|
|
163
|
+
deduped.push(item)
|
|
110
164
|
}
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
165
|
+
pool = deduped
|
|
166
|
+
skillPoolCacheSet(poolKey, pool)
|
|
120
167
|
}
|
|
168
|
+
const perPage = 20
|
|
169
|
+
const start = (page - 1) * perPage
|
|
170
|
+
items = pool.slice(start, start + perPage)
|
|
121
171
|
items = await enrichItems(items)
|
|
122
|
-
sendJson(res, 200, {
|
|
172
|
+
sendJson(res, 200, {
|
|
173
|
+
ok: true, query, items, authenticated: auth.loggedIn, source: 'github', skills: true,
|
|
174
|
+
// 技能池的真实总量(三 topic 并集去重后的条数),不是 GitHub 的 total_count 之和
|
|
175
|
+
// (三个 topic 之间有重叠,直接相加会把"共 N 条"报大)
|
|
176
|
+
...buildPageEnvelope({ shown: items.length, total: pool.length, page, perPage }),
|
|
177
|
+
})
|
|
123
178
|
return
|
|
124
179
|
}
|
|
125
180
|
if (source === 'gitee') {
|
|
@@ -140,13 +195,16 @@ async function routeSearch(req, res, rc) {
|
|
|
140
195
|
// repo 已由 githubRepoInfo 校验;分段编码(只编码中文等非 ASCII,斜杠保留原样——
|
|
141
196
|
// Gitee 服务器不认 %2F 编码的路径分隔,返回 404)
|
|
142
197
|
const [owner, name] = repo.split('/')
|
|
143
|
-
const data = await
|
|
198
|
+
const data = await fetchJson(`https://gitee.com/api/v5/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}${tokenQ}`)
|
|
144
199
|
items = normalizePlatformItems([data], 'master').map((item) => ({ ...item, source: 'gitee' }))
|
|
145
200
|
} catch (error) {
|
|
146
201
|
giteeError = error instanceof Error ? error.message : String(error)
|
|
147
202
|
}
|
|
148
203
|
}
|
|
149
|
-
sendJson(res, 200, {
|
|
204
|
+
sendJson(res, 200, {
|
|
205
|
+
ok: true, query, items, authenticated: auth.loggedIn, source, giteeNeedsLogin: false, directOnly: true, giteeError,
|
|
206
|
+
...buildPageEnvelope({ shown: items.length, total, page }),
|
|
207
|
+
})
|
|
150
208
|
return
|
|
151
209
|
} else if (source !== 'github') {
|
|
152
210
|
// 自定义搜索源:URL 模板({q}/{page} 占位符),返回数组或 {items} 结构;支持配置的请求头
|
|
@@ -159,7 +217,7 @@ async function routeSearch(req, res, rc) {
|
|
|
159
217
|
const url = custom.url
|
|
160
218
|
.replace('{q}', encodeURIComponent(query))
|
|
161
219
|
.replace('{page}', String(page))
|
|
162
|
-
const data = await
|
|
220
|
+
const data = await fetchJson(url, 15000, custom.headers ?? {})
|
|
163
221
|
items = normalizePlatformItems(data, 'main').map((item) => ({ ...item, source, sourceName: custom.name }))
|
|
164
222
|
} else {
|
|
165
223
|
// npm 包名搜索与 GitHub 仓库搜索**并行**:npm 侧要查 registry 搜索 + packument + 仓库元数据
|
|
@@ -167,12 +225,13 @@ async function routeSearch(req, res, rc) {
|
|
|
167
225
|
const npmPromise = raw !== '' && !body.skills
|
|
168
226
|
? searchNpmPackages(raw, orderedRegistries(readSources()), 3, auth.token).catch(() => [])
|
|
169
227
|
: Promise.resolve([])
|
|
170
|
-
const data = await
|
|
228
|
+
const data = await searchRepos(
|
|
171
229
|
`${GITHUB_API}/search/repositories?q=${encodeURIComponent(all ? query : `${query} topic:dsh-plugin`)}&sort=stars&order=desc&per_page=20&page=${page}`,
|
|
172
230
|
req.signal,
|
|
173
231
|
auth.token,
|
|
174
232
|
)
|
|
175
233
|
items = normalizePlatformItems(data.items ?? [], 'main').map((item) => ({ ...item, source: 'github' }))
|
|
234
|
+
if (typeof data?.total_count === 'number') total = data.total_count
|
|
176
235
|
items = await enrichItems(items)
|
|
177
236
|
// monorepo 子包增强(OpenViking/examples/dsh-memory-plugin 等可按子包名搜到;
|
|
178
237
|
// 代码搜索需登录,未登录时该函数返回空数组,见 domain/market.js 说明)
|
|
@@ -188,7 +247,7 @@ async function routeSearch(req, res, rc) {
|
|
|
188
247
|
// 首屏没有"名字逐词命中"的条目时,用 in:name,description,readme 再查一次(未登录也能用)。
|
|
189
248
|
if (raw !== '' && !body.skills && page === 1 && !hasDirectNameHit(items, raw)) {
|
|
190
249
|
try {
|
|
191
|
-
const again = await
|
|
250
|
+
const again = await searchRepos(
|
|
192
251
|
`${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
252
|
req.signal,
|
|
194
253
|
auth.token,
|
|
@@ -196,7 +255,9 @@ async function routeSearch(req, res, rc) {
|
|
|
196
255
|
for (const it of normalizePlatformItems(again.items ?? [], 'main')) {
|
|
197
256
|
if (items.some((x) => x.fullName === it.fullName)) continue
|
|
198
257
|
items.push({ ...it, source: 'github', viaReadme: true })
|
|
199
|
-
|
|
258
|
+
// 截断上限 40 是**既有**的检索面口径(不改"谁被搜出来"):只影响 in:readme 重查这一路,
|
|
259
|
+
// 且被截断这件事由下面的 envelope 如实告知(truncated/reason),不再静默。
|
|
260
|
+
if (items.length >= 40) { readmeTruncated = true; break }
|
|
200
261
|
}
|
|
201
262
|
} catch {}
|
|
202
263
|
}
|
|
@@ -217,7 +278,19 @@ async function routeSearch(req, res, rc) {
|
|
|
217
278
|
if (raw !== '' && !body.skills && !auth.loggedIn) extraNotes.codeSearchSkipped = true
|
|
218
279
|
}
|
|
219
280
|
items = items.filter((item) => item.fullName !== '')
|
|
220
|
-
|
|
281
|
+
// ★ 唯一一处排序 ★:所有搜索链(github 多路合并 / multi 多源 / custom / gitee)在**这里**统一
|
|
282
|
+
// 过同一个名字分层函数。上一版各分支自己排/自己截(skills 还截前 20),才会有"点两次结果不同"
|
|
283
|
+
// 与"高星无关仓库压过逐字命中"。
|
|
284
|
+
items = rankSearchResults(items, raw)
|
|
285
|
+
// 分页/截断如实回报:客户端据此决定「加载更多」是否可点、要不要明说"已截断"。
|
|
286
|
+
const envelope = buildPageEnvelope({ shown: items.length, total, page })
|
|
287
|
+
sendJson(res, 200, {
|
|
288
|
+
ok: true, query, items, authenticated: auth.loggedIn, source,
|
|
289
|
+
...envelope,
|
|
290
|
+
// 自己那一层 40 条上限截断了 → 明说(否则用户以为"就是没有";翻页也解不了这一层)
|
|
291
|
+
...(readmeTruncated ? { truncated: true, reason: envelope.reason !== null ? `${envelope.reason};另:README 重查层已取满 40 条` : 'README 重查层已取满 40 条(可换更精确的关键词缩小范围)' } : {}),
|
|
292
|
+
...(extraNotes),
|
|
293
|
+
})
|
|
221
294
|
return
|
|
222
295
|
}
|
|
223
296
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
5
5
|
import { dirname, join } from 'node:path'
|
|
6
6
|
import { createRequire } from 'node:module'
|
|
7
|
-
import { checkPluginFrameworkCompat, probePluginImport,
|
|
7
|
+
import { checkPluginFrameworkCompat, probePluginImport, readCompatPendingWithVerdicts, rowIdModuleMap, writeCompatPending } from '../domain/compat.js'
|
|
8
8
|
import { cleanFrameworkBackups, cleanFrameworkTopLevel, readRollbackReferenced, reportFrameworkEntities, resolveFrameworkLayout } from '../domain/framework-cleanup.js'
|
|
9
9
|
import { currentFrameworkVersion } from '../domain/framework.js'
|
|
10
10
|
import { pnpmRemove } from '../domain/install-job.js'
|
|
@@ -78,7 +78,8 @@ async function routeToggle(req, res, rc) {
|
|
|
78
78
|
// 前端弹风险提示确认框;带 confirmRisky:true 才放行)。
|
|
79
79
|
// 注意下面还有一条**硬**门禁:启用前的 import 冒烟检查——那条是事实性崩溃(模块根本加载不了),
|
|
80
80
|
// 不允许覆盖(否则下次启动必崩,与「服务永不崩」冲突)。
|
|
81
|
-
|
|
81
|
+
// 0.5.34:走**读取代发**(判据戳落后的结论先用当前判据复核/回写)⇒ 判定依据是最新的;软禁闸门本身一个字没放宽
|
|
82
|
+
const compatPending = readCompatPendingWithVerdicts({ baseDir: dirname(findPatchPath(ctx)) }).pending
|
|
82
83
|
const pend = (compatPending?.pending ?? []).find((p) => p.rowId === rowId && (p.status ?? 'pending') === 'pending')
|
|
83
84
|
if (pend) {
|
|
84
85
|
if (body.confirmRisky !== true) {
|
|
@@ -97,9 +98,9 @@ async function routeToggle(req, res, rc) {
|
|
|
97
98
|
return
|
|
98
99
|
}
|
|
99
100
|
try {
|
|
100
|
-
|
|
101
|
-
const rec = (
|
|
102
|
-
if (rec) { rec.riskyApprovedAt = Date.now(); writeCompatPending(
|
|
101
|
+
// 复核(读取代发)已经作用在 compatPending 上,直接用同一份对象写回,避免重复读+重复扫描
|
|
102
|
+
const rec = (compatPending?.pending ?? []).find((p) => p.rowId === rowId)
|
|
103
|
+
if (rec) { rec.riskyApprovedAt = Date.now(); writeCompatPending(compatPending) }
|
|
103
104
|
} catch {}
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -126,7 +127,7 @@ async function routeToggle(req, res, rc) {
|
|
|
126
127
|
// 用户实测就是这样(5 行)。check / checkNote / riskyApprovedAt 一律保留供事后查。
|
|
127
128
|
if (enabled) {
|
|
128
129
|
try {
|
|
129
|
-
const list =
|
|
130
|
+
const list = readCompatPendingWithVerdicts({ baseDir: dirname(findPatchPath(ctx)) }).pending
|
|
130
131
|
const meta = rowIdModuleMap(ctx).get(rowId) ?? null
|
|
131
132
|
if (list !== null && markPendingAdopted(list, rowId, 'manual-enable', meta)) {
|
|
132
133
|
list.updatedAt = new Date().toISOString()
|
|
@@ -304,14 +305,14 @@ async function routeAdaptUnlock(req, res, rc) {
|
|
|
304
305
|
sendError(res, 400, 'rowId 无效')
|
|
305
306
|
return
|
|
306
307
|
}
|
|
307
|
-
const
|
|
308
|
+
const patchPath = findPatchPath(ctx)
|
|
309
|
+
const profileDir = dirname(patchPath)
|
|
310
|
+
const { pending, verdicts } = readCompatPendingWithVerdicts({ baseDir: profileDir }) // 0.5.34:读取代发
|
|
308
311
|
const entry = (pending?.pending ?? []).find((p) => p.rowId === rowId && (p.status ?? 'pending') === 'pending')
|
|
309
312
|
if (entry === undefined) {
|
|
310
313
|
sendError(res, 404, '该行不在适配门待适配清单中(或已解锁)')
|
|
311
314
|
return
|
|
312
315
|
}
|
|
313
|
-
const patchPath = findPatchPath(ctx)
|
|
314
|
-
const profileDir = dirname(patchPath)
|
|
315
316
|
let pkg = null
|
|
316
317
|
let pkgDir = null
|
|
317
318
|
let version = null
|
|
@@ -327,11 +328,13 @@ async function routeAdaptUnlock(req, res, rc) {
|
|
|
327
328
|
const fwVer = typeof pending.frameworkVersion === 'string' ? pending.frameworkVersion : '?'
|
|
328
329
|
const check = pkg !== null ? checkPluginFrameworkCompat(pkg, fwVer, pkgDir) : { decision: 'unknown', reason: '无法读取包信息' }
|
|
329
330
|
if (check.decision === 'fail') {
|
|
330
|
-
// 0.5.33 改错 ①-2:文案必须写清「为什么不行 +
|
|
331
|
-
//
|
|
331
|
+
// 0.5.33 改错 ①-2:文案必须写清「为什么不行 + 出路」(旧文案只有一句"适配校验未通过",用户不知道下一步
|
|
332
|
+
// 该做什么);0.5.34:真不兼容的行**一个字都不放宽** —— 仍然 409、仍然零改动(补丁与清单逐字节不变)。
|
|
332
333
|
sendError(res, 409, `适配校验未通过:${String(check.reason ?? '适配校验未通过')}。出路:先点「检测更新」把它更新到兼容当前框架 ${fwVer} 的版本(更新完成会自动重跑源码扫描,通过即解锁);若它已不再需要,可在补丁里删掉/禁用这一行。本次未改动补丁与适配门清单。`)
|
|
333
334
|
return
|
|
334
335
|
}
|
|
336
|
+
// 可解锁的两种来路(判据都不放松):① 真的更新过(版本变化)② 旧判据的 fail 已被当前判据撤回
|
|
337
|
+
const view = verdicts.get(rowId) ?? null, rescinded = view?.rescinded === true, changed = version !== null && version !== entry.version
|
|
335
338
|
await removeDisableBlock(patchPath, rowId)
|
|
336
339
|
entry.status = 'adopted'
|
|
337
340
|
entry.adoptedAt = Date.now()
|
|
@@ -339,8 +342,9 @@ async function routeAdaptUnlock(req, res, rc) {
|
|
|
339
342
|
entry.adoptedFramework = fwVer
|
|
340
343
|
entry.check = check.decision
|
|
341
344
|
entry.checkNote = check.reason ?? null
|
|
345
|
+
entry.adoptedBasis = changed ? 'version-changed' : (rescinded ? 'verdict-rescinded' : 'scan-pass')
|
|
342
346
|
writeCompatPending(pending)
|
|
343
|
-
sendJson(res, 200, { ok: true, rowId, adopted: true, check: check.decision, note: check.reason ?? null })
|
|
347
|
+
sendJson(res, 200, { ok: true, rowId, adopted: true, check: check.decision, note: check.reason ?? null, basis: entry.adoptedBasis, previousVerdict: rescinded ? (view?.previous?.decision ?? 'fail') : null, evidence: rescinded ? (view?.evidence ?? null) : null })
|
|
344
348
|
return
|
|
345
349
|
}
|
|
346
350
|
|
|
@@ -357,7 +361,7 @@ async function routeAdaptUnlockAll(req, res, rc) {
|
|
|
357
361
|
sendError(res, 400, 'root 无效')
|
|
358
362
|
return
|
|
359
363
|
}
|
|
360
|
-
const pending =
|
|
364
|
+
const pending = readCompatPendingWithVerdicts({ baseDir: dirname(findPatchPath(ctx)) }).pending
|
|
361
365
|
const patchPath = findPatchPath(ctx)
|
|
362
366
|
const profileDir = dirname(patchPath)
|
|
363
367
|
// v0.3.45:匹配规则改成「moduleName 前缀 **或** 属主行集合」——
|
|
@@ -525,6 +529,13 @@ async function routeCleanResiduals(req, res, rc) {
|
|
|
525
529
|
extra.dryRun = dryRun
|
|
526
530
|
}
|
|
527
531
|
const payload = { ok: failed.length === 0, removed, kept, failed, count: removed.length, ...extra }
|
|
532
|
+
// 0.5.34 加法:框架档的**硬闸**结果必须如实浮到面板层(旧行为:改名了却没重建链接,只在 JSON 里留一行)
|
|
533
|
+
const fw = extra.framework
|
|
534
|
+
if (fw && (fw.ok === false || fw.verify?.ok === false)) {
|
|
535
|
+
const rb = (fw.rolledBack ?? []).length > 0 ? `;已回滚 ${fw.rolledBack.length} 条(${fw.rolledBack.map((r) => `${r.name}:${r.reason}`).join('、')})` : ''
|
|
536
|
+
sendJson(res, 200, { ...payload, ok: false, error: `${fw.verify?.ok === false ? `框架可解析性验证未通过(${fw.verify.error ?? '未知'})` : `有 ${fw.failed.length} 条没能安全落地`}${rb};详情见 framework 字段。` })
|
|
537
|
+
return
|
|
538
|
+
}
|
|
528
539
|
if (failed.length > 0) {
|
|
529
540
|
const detail = failed.slice(0, 3).map((f) => `${f.name}(${f.error ?? '未知原因'})`).join(';')
|
|
530
541
|
const keptNote = kept.length > 0 ? `;已在用插件 ${kept.length} 个(保留不删)` : ''
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { readFileSync, existsSync } from 'node:fs'
|
|
5
5
|
import { dirname, join } from 'node:path'
|
|
6
|
-
import { detectAdoptablePending, detectCompat, gatingSummary, readCompatGate,
|
|
6
|
+
import { detectAdoptablePending, detectCompat, gatingSummary, readCompatGate, readCompatPendingWithVerdicts } from '../domain/compat.js'
|
|
7
7
|
import { compUiUrl, findComponents } from '../domain/components.js'
|
|
8
8
|
import { currentFrameworkVersion, detectFrameworkUpgrade } from '../domain/framework.js'
|
|
9
9
|
import { peerVetoFor } from '../domain/peer-veto.js'
|
|
@@ -48,7 +48,11 @@ async function routeStateGet(req, res, rc) {
|
|
|
48
48
|
}
|
|
49
49
|
const extraRows = await readExtraBundleRows(dirname(patchPath))
|
|
50
50
|
const profileDir = dirname(patchPath)
|
|
51
|
-
|
|
51
|
+
// 0.5.34 加法(**读取代发**):给 profileDir ⇒ 判据戳落后的记录用当前判据只读重算,
|
|
52
|
+
// 结论变了才回写(幂等:结论不变**零写盘**)。真不兼容的重算仍 fail,一个字都不动。
|
|
53
|
+
const pendingState = readCompatPendingWithVerdicts({ baseDir: profileDir })
|
|
54
|
+
const compatPending = pendingState.pending
|
|
55
|
+
const compatVerdicts = pendingState.verdicts
|
|
52
56
|
const compatPendingRows = new Set((compatPending?.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => p.rowId))
|
|
53
57
|
// 兼容门总开关 + 自动检测(只提示不自动开)
|
|
54
58
|
const compatGate = readCompatGate()
|
|
@@ -72,6 +76,7 @@ async function routeStateGet(req, res, rc) {
|
|
|
72
76
|
if (!disabledByPatch && entry.fiberPhase === null && vetoFrameworkVersion !== null) {
|
|
73
77
|
try { veto = peerVetoFor({ moduleName: entry.moduleName, profileDir, frameworkVersion: vetoFrameworkVersion }) } catch { veto = null }
|
|
74
78
|
}
|
|
79
|
+
const verdict = compatVerdicts.get(entry.rowId) ?? null
|
|
75
80
|
return {
|
|
76
81
|
...entry,
|
|
77
82
|
userDisabled: disabledByPatch,
|
|
@@ -83,7 +88,13 @@ async function routeStateGet(req, res, rc) {
|
|
|
83
88
|
// v0.3.45:只有"补丁里此刻确实还禁着它"的行才显示【待适配】——
|
|
84
89
|
// 记录与开关脱节时(用户手动启用过 / 补丁块被清掉)不能继续挂着「待适配」误导人
|
|
85
90
|
pendingCompat: compatPendingRows.has(entry.rowId) && disabledByPatch,
|
|
91
|
+
// 0.5.34 加法:记录的 `fail` 若已被**当前判据**撤回(旧误报),面板据此不再说「待适配」,
|
|
92
|
+
// 改说「判据已撤回·可解锁」并给一键解锁入口(判据本身一个字都没放宽:真 fail 仍 fail)
|
|
93
|
+
compatRescinded: compatPendingRows.has(entry.rowId) && verdict?.rescinded === true,
|
|
94
|
+
// 结论视图:当前判据下的结论 + 是否过期/撤回/复核不了(面板要如实展示,不许把旧结论当事实)
|
|
95
|
+
compatVerdict: verdict,
|
|
86
96
|
// 自动检测结果(只提示,不自动开):该待适配行现在是否已适配(插件已更新 + 扫描通过)
|
|
97
|
+
// 或**旧判据的误报已被撤回**(0.5.34:不再要求"版本变化",否则用户被永久钉住)
|
|
87
98
|
adoptable: adoptable.get(entry.rowId) ?? null,
|
|
88
99
|
// 框架 peer 预检的本地复算结果(null = 无话可说;只在这类行上下发,不新增常驻面板行)
|
|
89
100
|
veto,
|
|
@@ -111,7 +122,7 @@ async function routeStateGet(req, res, rc) {
|
|
|
111
122
|
const selfPkg = JSON.parse(readFileSync(join(pluginRoot(), 'package.json'), 'utf8'))
|
|
112
123
|
selfVersion = typeof selfPkg.version === 'string' ? selfPkg.version : null
|
|
113
124
|
} catch {}
|
|
114
|
-
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null })) }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
125
|
+
sendJson(res, 200, { ok: true, entries, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? null } }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
115
126
|
return
|
|
116
127
|
}
|
|
117
128
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.35",
|
|
4
4
|
"description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|