@luolingp/request-doctor 0.14.0 → 0.14.2
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/client.js +252 -220
- package/docs/01-/350/275/257/344/273/266/351/234/200/346/261/202/350/247/204/346/240/274/350/257/264/346/230/216.md +1 -1
- package/docs/02-/350/275/257/344/273/266/350/256/276/350/256/241/350/257/264/346/230/216.md +392 -391
- package/docs/03-/350/275/257/344/273/266/346/265/213/350/257/225/346/212/245/345/221/212.md +4 -4
- package/docs/04-/345/256/211/350/243/205/351/203/250/347/275/262/350/257/264/346/230/216.md +1 -1
- package/docs/05-/347/224/250/346/210/267/346/211/213/345/206/214.md +30 -4
- package/docs/06-/350/275/257/344/273/266/350/264/250/351/207/217/345/272/246/351/207/217/346/212/245/345/221/212.md +1 -1
- package/docs/quality-report.json +8 -8
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -1,220 +1,252 @@
|
|
|
1
|
-
// 插件面板里的「不兼容安装」页。
|
|
2
|
-
//
|
|
3
|
-
// 管理器把版本检查放在安装流程里,对不上的包直接拒掉,然后原样回滚。它自己留了版本
|
|
4
|
-
// 豁免这个出口,但只有命令行或它自己的弹窗能走。这里把它做成一个页面:列出当前被拒的
|
|
5
|
-
// 插件,一键授权并安装,装完交给后台扫描去对齐声明、建兼容环境。
|
|
6
|
-
//
|
|
7
|
-
// 界面和 Host 之间走同源 HTTP,不碰 typert 的 remote 门面。动态加载的客户端模块拿不到
|
|
8
|
-
// 那个服务(它的上下文里没有),而 fetch 只要同源就行。Host 侧对应
|
|
9
|
-
// /doctor/incompatible 与 /doctor/install 两条路由。
|
|
10
|
-
window.__ModuleLoader__.load({
|
|
11
|
-
id: '@luolingp/request-doctor',
|
|
12
|
-
factory: (require) => {
|
|
13
|
-
const React = require('react')
|
|
14
|
-
const h = React.createElement
|
|
15
|
-
const inject = ['slots']
|
|
16
|
-
|
|
17
|
-
// 界面文案。语言服务不再参与:动态模块拿不到它,标签还曾经因此显示成键名。
|
|
18
|
-
const TEXT = {
|
|
19
|
-
tabLabel: '不兼容安装',
|
|
20
|
-
intro: '下面是被当前 DSH 版本拒装的插件。装它们只需要一步:先按管理器的规则授权这个版本,再让它自己装。装完之后,声明对齐和兼容环境会自动补上。',
|
|
21
|
-
placeholder: '包名@版本,例如 dsh-ssh-ops@0.4.0',
|
|
22
|
-
install: '授权并安装',
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
lines.push(h('div', { key:
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
})
|
|
1
|
+
// 插件面板里的「不兼容安装」页。
|
|
2
|
+
//
|
|
3
|
+
// 管理器把版本检查放在安装流程里,对不上的包直接拒掉,然后原样回滚。它自己留了版本
|
|
4
|
+
// 豁免这个出口,但只有命令行或它自己的弹窗能走。这里把它做成一个页面:列出当前被拒的
|
|
5
|
+
// 插件,一键授权并安装,装完交给后台扫描去对齐声明、建兼容环境。
|
|
6
|
+
//
|
|
7
|
+
// 界面和 Host 之间走同源 HTTP,不碰 typert 的 remote 门面。动态加载的客户端模块拿不到
|
|
8
|
+
// 那个服务(它的上下文里没有),而 fetch 只要同源就行。Host 侧对应
|
|
9
|
+
// /doctor/incompatible 与 /doctor/install 两条路由。
|
|
10
|
+
window.__ModuleLoader__.load({
|
|
11
|
+
id: '@luolingp/request-doctor',
|
|
12
|
+
factory: (require) => {
|
|
13
|
+
const React = require('react')
|
|
14
|
+
const h = React.createElement
|
|
15
|
+
const inject = ['slots']
|
|
16
|
+
|
|
17
|
+
// 界面文案。语言服务不再参与:动态模块拿不到它,标签还曾经因此显示成键名。
|
|
18
|
+
const TEXT = {
|
|
19
|
+
tabLabel: '不兼容安装',
|
|
20
|
+
intro: '下面是被当前 DSH 版本拒装的插件。装它们只需要一步:先按管理器的规则授权这个版本,再让它自己装。装完之后,声明对齐和兼容环境会自动补上。',
|
|
21
|
+
placeholder: '包名@版本,例如 dsh-ssh-ops@0.4.0',
|
|
22
|
+
install: '授权并安装',
|
|
23
|
+
allow: '只授权',
|
|
24
|
+
requires: '要求运行在',
|
|
25
|
+
none: '当前没有被拒装的插件。可以在上面直接填包名安装,或点「重新扫描」重算一遍。',
|
|
26
|
+
working: '正在处理',
|
|
27
|
+
done: '已装好,兼容环境会在后台补上',
|
|
28
|
+
failed: '没能装上',
|
|
29
|
+
rescan: '重新扫描',
|
|
30
|
+
restartTitle: '需要重启后生效',
|
|
31
|
+
sourceManager: '来源:插件管理器拒装',
|
|
32
|
+
sourceAudit: '来源:本插件审计',
|
|
33
|
+
sourceUnknown: '来源:未知',
|
|
34
|
+
verdict: '判定:',
|
|
35
|
+
missing: '缺符号:',
|
|
36
|
+
environment: '环境:',
|
|
37
|
+
offline: '连不上 Host 侧的安装路由,可以改用命令:',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const style = {
|
|
41
|
+
column: { display: 'flex', flexDirection: 'column', gap: 14, width: '100%', minWidth: 0 },
|
|
42
|
+
title: { margin: 0, fontSize: '0.95em', fontWeight: 600 },
|
|
43
|
+
text: { margin: 0, fontSize: '0.9em', lineHeight: 1.6, opacity: 0.8 },
|
|
44
|
+
hush: { margin: 0, fontSize: '0.9em', opacity: 0.7 },
|
|
45
|
+
mono: { margin: 0, fontSize: '0.85em', opacity: 0.75, fontFamily: 'monospace' },
|
|
46
|
+
line: { display: 'flex', gap: 8, alignItems: 'center' },
|
|
47
|
+
field: {
|
|
48
|
+
flex: 1, padding: '6px 10px', borderRadius: 8, font: 'inherit',
|
|
49
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))',
|
|
50
|
+
background: 'var(--dsw-alias-bg-l1, transparent)', color: 'inherit',
|
|
51
|
+
},
|
|
52
|
+
press: {
|
|
53
|
+
padding: '6px 14px', borderRadius: 8, cursor: 'pointer', font: 'inherit',
|
|
54
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.4))',
|
|
55
|
+
background: 'var(--dsw-alias-bg-l2, transparent)', color: 'inherit',
|
|
56
|
+
},
|
|
57
|
+
card: {
|
|
58
|
+
display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px', borderRadius: 10,
|
|
59
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))',
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 统一走同源路由:取回 JSON,失败就把原因抛出去,由调用方决定怎么显示。
|
|
64
|
+
async function requestJson(path, init) {
|
|
65
|
+
const response = await fetch(path, init)
|
|
66
|
+
return response.json()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 这一行是不是「审计说兼容、管理器仍拒装」。是的话要给授权按钮。
|
|
70
|
+
function hasConflict(row, conflicts) {
|
|
71
|
+
return conflicts.indexOf(row.name) >= 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 一行一条审计结论:判定、理由、缺的符号、环境。
|
|
75
|
+
// 没有结论的(只有管理器拒装)就什么都不加,不占地方。
|
|
76
|
+
function briefLines(brief, style) {
|
|
77
|
+
if (brief === undefined) return []
|
|
78
|
+
const lines = [h('div', { key: `v-${brief.verdict}`, style: style.hush }, `${TEXT.verdict} ${brief.verdict}`)]
|
|
79
|
+
for (const reason of (brief.reasons || []).slice(-2)) {
|
|
80
|
+
lines.push(h('div', { key: `r-${reason}`, style: style.hush }, reason))
|
|
81
|
+
}
|
|
82
|
+
if ((brief.missing || []).length > 0) {
|
|
83
|
+
lines.push(h('div', { key: 'missing', style: style.hush }, `${TEXT.missing} ${brief.missing.join(', ')}`))
|
|
84
|
+
}
|
|
85
|
+
if (brief.environment !== undefined) {
|
|
86
|
+
lines.push(h('div', { key: 'env', style: style.hush }, `${TEXT.environment} ${brief.environment}`))
|
|
87
|
+
}
|
|
88
|
+
return lines
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 这条是谁给的:管理器拒装和本插件审计,含义完全不同。
|
|
92
|
+
function sourceLabel(source) {
|
|
93
|
+
if (source === 'manager') return TEXT.sourceManager
|
|
94
|
+
if (source === 'audit') return TEXT.sourceAudit
|
|
95
|
+
return TEXT.sourceUnknown
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 读一次被拒插件清单。Host 侧已经把不可用的行筛掉了,
|
|
99
|
+
// 顺带回一段「已经修好但需要重启」的提示。
|
|
100
|
+
async function loadPanel() {
|
|
101
|
+
const data = await requestJson('/doctor/incompatible', { headers: { accept: 'application/json' } })
|
|
102
|
+
if (data.ok !== true) throw new Error(data.reason || 'host declined')
|
|
103
|
+
return {
|
|
104
|
+
rows: data.rows || [],
|
|
105
|
+
restart: data.restart || [],
|
|
106
|
+
briefs: data.briefs || {},
|
|
107
|
+
conflicts: data.conflicts || [],
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 装一个包。授权、重试、声明对齐都在 Host 侧完成,这里只拿结果。
|
|
112
|
+
// 两条写路由形状相同,只在路径上不同。
|
|
113
|
+
async function postSpec(path, spec) {
|
|
114
|
+
const data = await requestJson(path, {
|
|
115
|
+
method: 'POST',
|
|
116
|
+
headers: { 'content-type': 'application/json' },
|
|
117
|
+
body: JSON.stringify({ spec }),
|
|
118
|
+
})
|
|
119
|
+
return { ok: data.ok === true, reason: data.text || '' }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 装一个包。授权、重试、声明对齐都在 Host 侧完成,这里只拿结果。
|
|
123
|
+
async function postInstall(spec) {
|
|
124
|
+
return postSpec('/doctor/install', spec)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 让 Host 放行一个版本。冲突行上的按钮走这里,规则与 /doctor allow 相同。
|
|
128
|
+
async function postAllow(spec) {
|
|
129
|
+
return postSpec('/doctor/allow', spec)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// 让 Host 立刻重算一轮审计,不用等下一次定时扫描。
|
|
134
|
+
async function postRescan() {
|
|
135
|
+
const data = await requestJson('/doctor/rescan')
|
|
136
|
+
return { ok: data.ok === true, reason: data.reason || '' }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function IncompatiblePanel() {
|
|
140
|
+
const [rows, setRows] = React.useState([])
|
|
141
|
+
const [spec, setSpec] = React.useState('')
|
|
142
|
+
const [busy, setBusy] = React.useState(false)
|
|
143
|
+
const [note, setNote] = React.useState('')
|
|
144
|
+
const [restart, setRestart] = React.useState([])
|
|
145
|
+
const [briefs, setBriefs] = React.useState({})
|
|
146
|
+
const [conflicts, setConflicts] = React.useState([])
|
|
147
|
+
|
|
148
|
+
const refresh = React.useCallback(async () => {
|
|
149
|
+
try {
|
|
150
|
+
const panel = await loadPanel()
|
|
151
|
+
setRows(panel.rows)
|
|
152
|
+
setRestart(panel.restart)
|
|
153
|
+
setBriefs(panel.briefs || {})
|
|
154
|
+
setConflicts(panel.conflicts || [])
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// 路由不通就把可用的替代办法说出来,不留空白。
|
|
157
|
+
setNote(`${TEXT.offline} /doctor install <包名>(${String((error && error.message) || error)})`)
|
|
158
|
+
}
|
|
159
|
+
}, [])
|
|
160
|
+
|
|
161
|
+
React.useEffect(() => { refresh() }, [refresh])
|
|
162
|
+
|
|
163
|
+
// 两个入口共用同一段收尾:跑、报结果、刷新列表。
|
|
164
|
+
async function run(label, task) {
|
|
165
|
+
setBusy(true)
|
|
166
|
+
setNote(`${TEXT.working} ${label}`)
|
|
167
|
+
try {
|
|
168
|
+
const outcome = await task()
|
|
169
|
+
setNote(outcome.ok ? `${TEXT.done}:${label}` : `${TEXT.failed}:${outcome.reason}`)
|
|
170
|
+
await refresh()
|
|
171
|
+
} catch (error) {
|
|
172
|
+
setNote(`${TEXT.failed}:${String(error)}`)
|
|
173
|
+
} finally {
|
|
174
|
+
setBusy(false)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const submit = () => {
|
|
179
|
+
const trimmed = spec.trim()
|
|
180
|
+
if (trimmed === '') return
|
|
181
|
+
run(trimmed, () => postInstall(trimmed))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const children = []
|
|
185
|
+
children.push(h('p', { key: 'title', style: style.title }, TEXT.tabLabel))
|
|
186
|
+
children.push(h('p', { key: 'intro', style: style.text }, TEXT.intro))
|
|
187
|
+
children.push(h('div', { key: 'entry', style: style.line },
|
|
188
|
+
h('input', {
|
|
189
|
+
value: spec, style: style.field, placeholder: TEXT.placeholder,
|
|
190
|
+
onChange: (event) => setSpec(event.target.value),
|
|
191
|
+
}),
|
|
192
|
+
h('button', {
|
|
193
|
+
type: 'button', style: style.press, disabled: busy, onClick: submit,
|
|
194
|
+
}, TEXT.install),
|
|
195
|
+
h('button', {
|
|
196
|
+
type: 'button', style: style.press, disabled: busy,
|
|
197
|
+
onClick: () => run(TEXT.rescan, postRescan),
|
|
198
|
+
style: { ...style.press, opacity: 0.8 },
|
|
199
|
+
}, TEXT.rescan)))
|
|
200
|
+
|
|
201
|
+
if (rows.length === 0) {
|
|
202
|
+
children.push(h('p', { key: 'none', style: style.hush }, TEXT.none))
|
|
203
|
+
} else {
|
|
204
|
+
children.push(h('div', { key: 'rows', style: style.column }, rows.map((row) => h('div', {
|
|
205
|
+
key: `${row.name}@${row.version}`, style: style.card,
|
|
206
|
+
},
|
|
207
|
+
h('div', { style: style.title }, `${row.name}@${row.version}`),
|
|
208
|
+
h('div', { style: style.hush }, `${TEXT.requires} ${row.runtimeVersion || '?'}`),
|
|
209
|
+
h('div', { style: style.hush }, sourceLabel(row.source)),
|
|
210
|
+
...briefLines(briefs[row.name], style),
|
|
211
|
+
|
|
212
|
+
hasConflict(row, conflicts)
|
|
213
|
+
? h('button', {
|
|
214
|
+
type: 'button', style: style.press, disabled: busy,
|
|
215
|
+
onClick: () => run(`${row.name}@${row.version}`, () => postAllow(`${row.name}@${row.version}`)),
|
|
216
|
+
}, TEXT.allow)
|
|
217
|
+
: h('span'),
|
|
218
|
+
h('button', {
|
|
219
|
+
type: 'button', style: style.press, disabled: busy,
|
|
220
|
+
onClick: () => run(`${row.name}@${row.version}`, () => postInstall(`${row.name}@${row.version}`)),
|
|
221
|
+
}, TEXT.install)))))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if (restart.length > 0) {
|
|
226
|
+
children.push(h('p', { key: 'restart-title', style: style.title }, TEXT.restartTitle))
|
|
227
|
+
for (const item of restart) {
|
|
228
|
+
children.push(h('p', { key: `restart-${item.name}`, style: style.hush }, item.hint))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Host 的返回是多行文本(安装结果、位置、兼容环境),按原样保留换行。
|
|
232
|
+
if (note !== '') children.push(h('p', { key: 'note', style: { ...style.hush, whiteSpace: 'pre-wrap' } }, note))
|
|
233
|
+
return h('div', { style: style.column }, ...children)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function apply(ctx) {
|
|
237
|
+
if (typeof ctx.slots?.inject !== 'function') return
|
|
238
|
+
const cells = [
|
|
239
|
+
['plugins.tab', 'requestDoctorIncompatible', 5, TEXT.tabLabel, IncompatiblePanel],
|
|
240
|
+
['settings.plugins.tab', 'requestDoctorSettingsTab', 20, TEXT.tabLabel, IncompatiblePanel],
|
|
241
|
+
]
|
|
242
|
+
for (const [slot, id, order, label, Component] of cells) {
|
|
243
|
+
ctx.slots.inject(slot, () => ctx.slots.register({ name: slot, id, order, label }, Component))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const module = { exports: {} }
|
|
248
|
+
module.exports.apply = apply
|
|
249
|
+
module.exports.inject = inject
|
|
250
|
+
return module.exports
|
|
251
|
+
},
|
|
252
|
+
})
|