dsh-remote-plugin 0.7.1 → 0.7.3
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/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +322 -19
- package/index.mjs +41 -9
- package/insights.mjs +73 -0
- package/package.json +3 -2
- package/plugin-center.mjs +118 -21
- package/public/admin.html +3 -0
- package/public/admin.js +1 -0
- package/public/app.js +403 -470
- package/public/desktop/desktop.css +101 -0
- package/public/desktop/desktop.html +63 -20
- package/public/desktop/desktop.js +324 -99
- package/public/history.js +160 -0
- package/public/index.html +73 -84
- package/public/insights.css +14 -0
- package/public/insights.js +228 -0
- package/public/link-check.js +109 -0
- package/public/plugin-center.css +100 -1
- package/public/plugin-center.js +222 -60
- package/public/plugin.js +7 -1
- package/public/styles.css +107 -95
- package/public/update.json +12 -12
- package/public/version.json +1 -1
package/public/plugin-center.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
window.DshPluginCenter = (() => {
|
|
4
4
|
let dialog, connection, snapshot, timer, generation = 0, pending = false, tab = 'installed', offset = 0, searchSequence = 0, detailSequence = 0
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const expandedDetails = new Map()
|
|
6
|
+
let renderedInstalled = '', renderedJobs = '', filter = '', statusFilter = 'all', refreshSequence = 0
|
|
7
|
+
const labels = { install: '安装', update: '更新', remove: '卸载', enable: '启用', disable: '停用', queued: '等待执行', running: '执行中', complete: '已完成', failed: '失败', interrupted: '已中断', active: '运行中', pending: '等待依赖', loading: '加载中', disposed: '未运行', unloading: '卸载中', inactive: '未运行' }
|
|
7
8
|
function node(tag, text, className) {
|
|
8
9
|
const el = document.createElement(tag)
|
|
9
10
|
if (text != null) el.textContent = text
|
|
@@ -14,7 +15,7 @@ window.DshPluginCenter = (() => {
|
|
|
14
15
|
const el = node('button', text)
|
|
15
16
|
el.type = 'button'; el.addEventListener('click', () => {
|
|
16
17
|
const current = generation
|
|
17
|
-
Promise.resolve().then(action).catch(error => { if (current === generation) message(error.message) })
|
|
18
|
+
Promise.resolve().then(() => { if (current === generation) return action() }).catch(error => { if (current === generation) message(error.message) })
|
|
18
19
|
})
|
|
19
20
|
return el
|
|
20
21
|
}
|
|
@@ -35,37 +36,152 @@ window.DshPluginCenter = (() => {
|
|
|
35
36
|
}
|
|
36
37
|
function card(name, description) {
|
|
37
38
|
const el = node('article', null, 'pc-card')
|
|
38
|
-
|
|
39
|
+
const header = node('div', null, 'pc-card-head')
|
|
40
|
+
const icon = node('span', (name || 'P').replace(/^@/, '').slice(0, 1).toUpperCase(), 'pc-plugin-icon')
|
|
41
|
+
icon.setAttribute('aria-hidden', 'true')
|
|
42
|
+
header.append(icon, node('h3', name)); el.append(header)
|
|
43
|
+
if (description) el.append(node('p', description, 'pc-description'))
|
|
39
44
|
return el
|
|
40
45
|
}
|
|
41
46
|
function renderInstalled() {
|
|
42
|
-
const list = dialog.querySelector('.pc-list'); list.replaceChildren()
|
|
43
|
-
if (!snapshot
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
const list = dialog.querySelector('.pc-list'), restore = preserveListPosition(list); list.replaceChildren()
|
|
48
|
+
if (!snapshot) { list.append(node('p', '尚未读取插件状态,请点击刷新。')); return }
|
|
49
|
+
const rows = snapshot.items.filter(item => tab === 'builtin' ? !item.managed : item.managed)
|
|
50
|
+
.filter(item => [item.name, item.displayName, item.description].join(' ').toLowerCase().includes(filter.toLowerCase()))
|
|
51
|
+
.filter(item => statusFilter === 'all' || (statusFilter === 'enabled' ? item.enabled : !item.enabled))
|
|
52
|
+
if (!rows.length) list.append(node('p', filter || statusFilter !== 'all' ? '没有匹配的插件。' : tab === 'builtin' ? '暂无内置插件。' : '还没有安装其他插件,点击“添加插件”查看详情并安装。', 'pc-empty'))
|
|
53
|
+
for (const item of rows) {
|
|
54
|
+
const el = card(item.displayName || item.name, item.description)
|
|
55
|
+
el.append(node('small', item.name, 'pc-package'))
|
|
56
|
+
const meta = node('div', null, 'pc-meta')
|
|
57
|
+
meta.append(node('span', item.version ? 'v' + item.version : item.requested || '内置', 'pc-version'))
|
|
58
|
+
if (!item.bundle || !item.managed || !snapshot.writable) meta.append(node('span', item.enabled ? '配置启用' : '配置停用', 'pc-badge ' + (item.enabled ? 'is-active' : '')))
|
|
59
|
+
const runtime = item.runtime || []
|
|
60
|
+
if (runtime.some(entry => entry.phase === 'failed')) meta.append(node('span', '运行异常', 'pc-badge is-error'))
|
|
61
|
+
else if (runtime.length) meta.append(node('span', `${runtime.filter(entry => entry.phase === 'active').length}/${runtime.length} 组件运行中`, 'pc-badge'))
|
|
62
|
+
el.append(meta)
|
|
63
|
+
const actions = node('div', null, 'pc-card-actions')
|
|
64
|
+
const area = inlineArea(item.name)
|
|
65
|
+
const detail = button('查看详情', () => installedDetails(item, area, detail)); detail.className = 'pc-detail-button'
|
|
66
|
+
actions.append(detail)
|
|
47
67
|
if (item.managed && snapshot.writable) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
actions.append(button('查看更新', () => showDetails(item.name, '', area, detail, true)))
|
|
69
|
+
if (item.bundle) {
|
|
70
|
+
const toggle = button(item.enabled ? '已启用' : '已停用', () => mutate(item.enabled ? 'disable' : 'enable', item.name))
|
|
71
|
+
toggle.className = 'pc-toggle'; toggle.disabled = pending || snapshot.busy
|
|
72
|
+
toggle.setAttribute('role', 'switch'); toggle.setAttribute('aria-checked', String(item.enabled)); toggle.setAttribute('aria-label', '启用 ' + item.name)
|
|
73
|
+
toggle.setAttribute('title', item.enabled ? '配置已启用' : '配置已停用')
|
|
74
|
+
el.querySelector('.pc-card-head').append(toggle)
|
|
75
|
+
}
|
|
76
|
+
const remove = button('卸载', () => mutate('remove', item.name)); remove.className = 'pc-danger-button'; actions.append(remove)
|
|
77
|
+
for (const action of actions.children) if (action !== detail) action.disabled = pending || snapshot.busy
|
|
78
|
+
} else actions.append(node('small', '主机维护 · 只读'))
|
|
79
|
+
el.dataset.plugin = item.name
|
|
80
|
+
detail.setAttribute('aria-controls', area.id)
|
|
81
|
+
renderInline(area, detail, item)
|
|
82
|
+
el.append(actions, area)
|
|
55
83
|
list.append(el)
|
|
56
84
|
}
|
|
57
|
-
const runtime = node('details')
|
|
85
|
+
const runtime = node('details', null, 'pc-runtime')
|
|
58
86
|
runtime.append(node('summary', '当前进程实际加载状态 (' + snapshot.runtime.length + ')'))
|
|
59
87
|
if (!snapshot.runtimeAvailable) runtime.append(node('p', '此 DSH 版本未提供加载状态'))
|
|
60
88
|
for (const entry of snapshot.runtime) runtime.append(node('p', entry.name + ' · ' + entry.phase + (entry.enabled ? '' : ' · disabled')))
|
|
61
89
|
list.append(runtime)
|
|
90
|
+
restore()
|
|
91
|
+
}
|
|
92
|
+
function preserveListPosition(list) {
|
|
93
|
+
const anchor = [...list.children].find(el => el.dataset.plugin && el.getBoundingClientRect?.().bottom > 0)
|
|
94
|
+
if (!anchor?.getBoundingClientRect) return () => {}
|
|
95
|
+
const top = anchor.getBoundingClientRect().top, name = anchor.dataset.plugin
|
|
96
|
+
let parent = list.parentElement
|
|
97
|
+
while (parent && !/(auto|scroll)/.test(getComputedStyle(parent).overflowY)) parent = parent.parentElement
|
|
98
|
+
return () => {
|
|
99
|
+
const current = [...list.children].find(el => el.dataset.plugin === name)
|
|
100
|
+
if (!current) return
|
|
101
|
+
const delta = current.getBoundingClientRect().top - top
|
|
102
|
+
if (parent) parent.scrollTop += delta
|
|
103
|
+
else window.scrollBy(0, delta)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function inlineArea(name) {
|
|
107
|
+
const area = node('section', null, 'pc-inline-detail')
|
|
108
|
+
area.dataset.key = tab + ':' + name
|
|
109
|
+
area.id = 'pc-detail-' + encodeURIComponent(area.dataset.key)
|
|
110
|
+
return area
|
|
111
|
+
}
|
|
112
|
+
function currentArea(key) { return [...dialog.querySelectorAll('.pc-inline-detail')].find(el => el.dataset.key === key) }
|
|
113
|
+
function renderInline(area, control, installed) {
|
|
114
|
+
const entry = expandedDetails.get(area.dataset.key)
|
|
115
|
+
control.textContent = entry ? '收起详情' : '查看详情'
|
|
116
|
+
control.setAttribute('aria-expanded', String(!!entry))
|
|
117
|
+
area.replaceChildren(); area.hidden = !entry
|
|
118
|
+
if (!entry) return
|
|
119
|
+
if (entry.loading) { area.append(node('p', '正在加载详情…', 'pc-detail-loading')); return }
|
|
120
|
+
if (entry.error) { area.append(node('p', entry.error, 'pc-detail-error'), button('重试', () => showDetails(entry.name, entry.version, area, control, true))); return }
|
|
121
|
+
if (entry.kind === 'installed') {
|
|
122
|
+
const item = installed || entry.item
|
|
123
|
+
if (item.description) area.append(node('p', item.description))
|
|
124
|
+
area.append(node('p', '配置:' + (item.enabled ? '启用' : '停用')), node('h4', '组件运行状态'))
|
|
125
|
+
const entries = item.runtime || []
|
|
126
|
+
if (!entries.length) area.append(node('p', '未获得该包的组件归属。可在列表底部查看当前进程全部加载状态。'))
|
|
127
|
+
for (const component of entries) area.append(node('p', component.name + ' · ' + (labels[component.phase] || component.phase)))
|
|
128
|
+
if (!item.managed) area.append(node('p', '随 DSH 提供的核心插件由主机维护。'))
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const item = entry.item
|
|
132
|
+
area.append(node('h4', item.name + ' @ ' + item.version), node('p', item.description || ''), node('p', '许可证:' + (item.license || '未声明') + ' · ' + (item.bundle ? 'DSH bundle' : '未声明 DSH bundle')))
|
|
133
|
+
area.append(node('p', '运行要求:' + JSON.stringify(item.engines) + ';依赖要求:' + JSON.stringify(item.peers)))
|
|
134
|
+
area.append(node('p', '插件代码将在 DSH 主机运行。安装脚本默认禁用;需要额外配置的插件须在主机完成配置。'))
|
|
135
|
+
if (/^https:\/\//.test(item.homepage)) {
|
|
136
|
+
const link = node('a', '项目主页'); link.href = item.homepage; link.target = '_blank'; link.rel = 'noopener noreferrer'; area.append(link)
|
|
137
|
+
}
|
|
138
|
+
const input = node('input'); input.value = entry.versionDraft ?? item.version; input.setAttribute('aria-label', '插件版本'); input.placeholder = '指定版本,如 1.2.3'
|
|
139
|
+
input.addEventListener('input', () => { entry.versionDraft = input.value })
|
|
140
|
+
area.append(input, button('查看此版本', () => showDetails(entry.name, input.value.trim(), area, control, true)))
|
|
141
|
+
const row = snapshot?.items.find(row => row.name === item.name), action = row ? 'update' : 'install'
|
|
142
|
+
const install = button(labels[action] + ' ' + item.version, () => mutate(action, item.name, item.version))
|
|
143
|
+
install.disabled = !item.bundle || item.protected || !snapshot?.writable || snapshot.busy || pending || row?.version === item.version
|
|
144
|
+
area.append(install)
|
|
145
|
+
}
|
|
146
|
+
function installedDetails(item, area, control) {
|
|
147
|
+
if (expandedDetails.has(area.dataset.key)) expandedDetails.delete(area.dataset.key)
|
|
148
|
+
else expandedDetails.set(area.dataset.key, { kind: 'installed', item })
|
|
149
|
+
renderInline(area, control, item)
|
|
150
|
+
}
|
|
151
|
+
function renderTabs() {
|
|
152
|
+
for (const button of dialog.querySelectorAll('[data-pc-tab]')) {
|
|
153
|
+
button.classList.toggle('active', button.dataset.pcTab === tab)
|
|
154
|
+
button.setAttribute('aria-pressed', String(button.dataset.pcTab === tab))
|
|
155
|
+
}
|
|
62
156
|
}
|
|
63
157
|
async function refresh() {
|
|
64
|
-
|
|
158
|
+
const sequence = ++refreshSequence
|
|
159
|
+
let data
|
|
160
|
+
try { data = await api('/state') } catch (error) { if (sequence === refreshSequence) throw error; return }
|
|
161
|
+
if (sequence !== refreshSequence) return
|
|
162
|
+
snapshot = data
|
|
163
|
+
const recovery = dialog.querySelector('.pc-recovery'); recovery.replaceChildren()
|
|
164
|
+
if (snapshot.recovery) {
|
|
165
|
+
recovery.append(node('p', snapshot.recovery.reason))
|
|
166
|
+
const action = button('恢复旧任务', () => {
|
|
167
|
+
const body = { id: snapshot.recovery.id, revision: snapshot.revision, confirmNoRunningProcess: true }
|
|
168
|
+
const review = card('解除旧任务锁', '请先在主机确认没有正在运行的插件安装或卸载进程。此操作保留文件和备份,不执行回滚。')
|
|
169
|
+
review.append(button('已确认,解除锁定', () => executeMutation(body, '/recover')), button('取消', () => dialog.querySelector('.pc-detail').replaceChildren()))
|
|
170
|
+
dialog.querySelector('.pc-detail').replaceChildren(review)
|
|
171
|
+
})
|
|
172
|
+
action.disabled = !snapshot.writable || pending; recovery.append(action)
|
|
173
|
+
}
|
|
65
174
|
dialog.querySelector('.pc-profile').textContent = snapshot.profile ? '当前环境:' + snapshot.profile : '当前环境无法识别'
|
|
66
175
|
dialog.querySelector('.pc-status').textContent = snapshot.reason || (snapshot.pendingRestart ? '插件配置已变更,重启 DSH 后生效。' : '安装到当前连接的 DSH 主机。')
|
|
67
|
-
|
|
68
|
-
|
|
176
|
+
dialog.querySelector('.pc-status').hidden = !snapshot.reason && !snapshot.pendingRestart
|
|
177
|
+
const summary = dialog.querySelector('.pc-summary'); summary.replaceChildren()
|
|
178
|
+
for (const [count, title] of [[snapshot.items.filter(item => item.managed).length, '已安装'], [snapshot.items.filter(item => item.managed && item.enabled).length, '配置启用']]) {
|
|
179
|
+
const stat = node('span', null, 'pc-stat'); stat.append(node('strong', String(count)), node('span', title)); summary.append(stat)
|
|
180
|
+
}
|
|
181
|
+
if (snapshot.busy) summary.append(node('span', '任务执行中', 'pc-badge'))
|
|
182
|
+
const installedKey = JSON.stringify([snapshot.items, snapshot.runtime, snapshot.pendingRestart, snapshot.busy, snapshot.writable, pending, tab, filter, statusFilter])
|
|
183
|
+
if (tab !== 'market' && installedKey !== renderedInstalled) { renderInstalled(); renderedInstalled = installedKey }
|
|
184
|
+
renderTabs()
|
|
69
185
|
const jobsKey = JSON.stringify(snapshot.operations)
|
|
70
186
|
if (jobsKey === renderedJobs) return
|
|
71
187
|
renderedJobs = jobsKey
|
|
@@ -88,36 +204,40 @@ window.DshPluginCenter = (() => {
|
|
|
88
204
|
const list = dialog.querySelector('.pc-list'); list.replaceChildren(node('p', '正在查询 npm 插件目录…'))
|
|
89
205
|
const data = await api('/market?q=' + encodeURIComponent(q) + '&offset=' + offset)
|
|
90
206
|
if (tab !== 'market' || sequence !== searchSequence) return
|
|
91
|
-
list.replaceChildren(node('p', '来源:npm · 安装前校验 DSH bundle。目录收录不代表安全或兼容性审核。'))
|
|
207
|
+
list.replaceChildren(node('p', '来源:npm · 安装前校验 DSH bundle。目录收录不代表安全或兼容性审核。', 'pc-market-note'))
|
|
92
208
|
if (!data.items.length) list.append(node('p', '没有找到插件;也可直接输入完整 npm 包名后查看详情。'))
|
|
93
209
|
for (const item of data.items) {
|
|
94
210
|
const el = card(item.name, item.description)
|
|
95
|
-
|
|
211
|
+
const meta = node('div', null, 'pc-meta'); meta.append(node('span', item.version ? 'v' + item.version : '版本未声明', 'pc-version'))
|
|
212
|
+
const actions = node('div', null, 'pc-card-actions')
|
|
213
|
+
const area = inlineArea(item.name)
|
|
214
|
+
const detail = button('查看详情', () => showDetails(item.name, '', area, detail)); detail.className = 'pc-detail-button'; detail.setAttribute('aria-controls', area.id); actions.append(detail)
|
|
215
|
+
el.dataset.plugin = item.name
|
|
216
|
+
renderInline(area, detail)
|
|
217
|
+
el.append(meta, actions, area)
|
|
96
218
|
list.append(el)
|
|
97
219
|
}
|
|
98
220
|
if (offset > 0) list.append(button('上一页', () => { offset -= 20; return search() }))
|
|
99
221
|
if (offset + 20 < data.total) list.append(button('下一页', () => { offset += 20; return search() }))
|
|
100
222
|
}
|
|
101
|
-
async function showDetails(name, version) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
223
|
+
async function showDetails(name, version, area, control, force = false) {
|
|
224
|
+
const key = area.dataset.key
|
|
225
|
+
if (!force && expandedDetails.has(key)) { expandedDetails.delete(key); renderInline(area, control); return }
|
|
226
|
+
const current = generation, entry = { name, version, kind: 'market', loading: true }
|
|
227
|
+
expandedDetails.set(key, entry); renderInline(area, control)
|
|
228
|
+
try {
|
|
229
|
+
const { item } = await api('/details?name=' + encodeURIComponent(name) + (version ? '&version=' + encodeURIComponent(version) : ''))
|
|
230
|
+
if (current !== generation || expandedDetails.get(key) !== entry) return
|
|
231
|
+
entry.item = item; entry.loading = false
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (current !== generation || expandedDetails.get(key) !== entry) return
|
|
234
|
+
entry.loading = false; entry.error = error.message
|
|
235
|
+
}
|
|
236
|
+
const target = currentArea(key)
|
|
237
|
+
if (target) {
|
|
238
|
+
const button = (target.parentElement || target.parent).querySelector('.pc-detail-button')
|
|
239
|
+
renderInline(target, button)
|
|
112
240
|
}
|
|
113
|
-
const input = node('input'); input.value = item.version; input.setAttribute('aria-label', '插件版本'); input.placeholder = '指定版本,如 1.2.3'
|
|
114
|
-
el.append(input, button('查看此版本', () => showDetails(name, input.value.trim())))
|
|
115
|
-
const installed = snapshot?.items.find(row => row.name === name)
|
|
116
|
-
const action = installed ? 'update' : 'install'
|
|
117
|
-
const install = button((labels[action]) + ' ' + item.version, () => mutate(action, name, item.version))
|
|
118
|
-
install.disabled = !item.bundle || item.protected || !snapshot?.writable || snapshot.busy || pending || installed?.version === item.version
|
|
119
|
-
el.append(install, button('关闭详情', () => area.replaceChildren()))
|
|
120
|
-
area.append(el); area.scrollIntoView({ block: 'nearest' })
|
|
121
241
|
}
|
|
122
242
|
async function mutate(action, name, version) {
|
|
123
243
|
if (pending) return
|
|
@@ -127,55 +247,97 @@ window.DshPluginCenter = (() => {
|
|
|
127
247
|
const area = dialog.querySelector('.pc-detail')
|
|
128
248
|
const review = card('确认' + labels[action], name + (version ? '@' + version : ''))
|
|
129
249
|
review.append(node('p', '目标环境:' + snapshot.profile + '。配置修改后需要重启 DSH;不会自动重启。'))
|
|
130
|
-
|
|
250
|
+
const confirm = button('确认' + labels[action], () => executeMutation(body)); confirm.className = action === 'remove' ? 'pc-danger-confirm' : 'pc-primary'
|
|
251
|
+
review.append(confirm, button('取消', () => area.replaceChildren()))
|
|
131
252
|
area.replaceChildren(review); area.scrollIntoView({ block: 'nearest' })
|
|
132
253
|
}
|
|
133
|
-
async function executeMutation(body) {
|
|
254
|
+
async function executeMutation(body, endpoint = '/operations') {
|
|
134
255
|
if (pending) return
|
|
135
256
|
const current = generation
|
|
136
257
|
pending = true
|
|
137
258
|
try {
|
|
138
259
|
message('正在提交…')
|
|
139
|
-
await api(
|
|
260
|
+
await api(endpoint, body)
|
|
140
261
|
message('已受理,可在操作记录查看结果。')
|
|
141
262
|
dialog.querySelector('.pc-detail').replaceChildren()
|
|
142
263
|
} catch (error) {
|
|
143
264
|
if (current !== generation) return
|
|
144
265
|
message(error.message + ';请先查看操作记录,避免重复提交。')
|
|
145
|
-
const retry = button('重试同一请求', () => executeMutation(body))
|
|
266
|
+
const retry = button('重试同一请求', () => executeMutation(body, endpoint))
|
|
146
267
|
dialog.querySelector('.pc-detail').replaceChildren(retry)
|
|
147
268
|
} finally { if (current === generation) { pending = false; await refresh() } }
|
|
148
269
|
}
|
|
149
|
-
|
|
270
|
+
function close() {
|
|
271
|
+
generation++; searchSequence++; detailSequence++; clearTimeout(timer); expandedDetails.clear()
|
|
272
|
+
if (!dialog) return
|
|
273
|
+
if (dialog.tagName === 'DIALOG') dialog.close()
|
|
274
|
+
else { dialog.open = false; dialog.remove() }
|
|
275
|
+
dialog = null
|
|
276
|
+
}
|
|
277
|
+
async function open(config, target) {
|
|
278
|
+
close()
|
|
150
279
|
if (!dialog) {
|
|
151
|
-
dialog = node('dialog', null, 'pc-dialog')
|
|
152
|
-
const heading = node('div', null, 'pc-heading')
|
|
153
|
-
const
|
|
154
|
-
|
|
280
|
+
dialog = node(target ? 'div' : 'dialog', null, 'pc-dialog' + (target ? ' pc-page' : ''))
|
|
281
|
+
const heading = node('div', null, 'pc-heading')
|
|
282
|
+
const title = node('div', null, 'pc-title'); title.append(node('h2', '插件管理'), node('p', '管理当前主机的插件与扩展', 'pc-intro')); heading.append(title)
|
|
283
|
+
if (!target) heading.append(button('关闭', close))
|
|
284
|
+
const controls = node('div', null, 'pc-controls')
|
|
285
|
+
const toolbar = node('div', null, 'pc-tabs')
|
|
286
|
+
for (const [value, title] of [['installed', '已安装'], ['market', '添加插件'], ['builtin', '内置插件']]) {
|
|
287
|
+
const control = button(title, () => {
|
|
288
|
+
tab = value; detailSequence++; expandedDetails.clear(); renderedInstalled = ''; dialog.querySelector('.pc-detail').replaceChildren()
|
|
289
|
+
renderTabs(); message('')
|
|
290
|
+
dialog.querySelector('.pc-market-tools').hidden = value !== 'market'; dialog.querySelector('.pc-filters').hidden = value === 'market'
|
|
291
|
+
if (value === 'market') { offset = 0; return search() }
|
|
292
|
+
return refresh()
|
|
293
|
+
})
|
|
294
|
+
control.dataset.pcTab = value; toolbar.append(control)
|
|
295
|
+
}
|
|
296
|
+
const refreshButton = button('刷新', () => tab === 'market' ? search() : refresh()); refreshButton.className = 'pc-refresh'
|
|
297
|
+
const toolbarRow = node('div', null, 'pc-toolbar'); toolbarRow.append(toolbar, refreshButton)
|
|
298
|
+
const filters = node('div', null, 'pc-filters')
|
|
299
|
+
const query = node('input'); query.placeholder = '筛选已安装插件'; query.setAttribute('aria-label', '筛选已安装插件')
|
|
300
|
+
query.addEventListener('input', () => { filter = query.value.trim(); renderInstalled() })
|
|
301
|
+
const select = node('select'); select.setAttribute('aria-label', '按配置状态筛选')
|
|
302
|
+
for (const [value, title] of [['all', '全部状态'], ['enabled', '配置启用'], ['disabled', '配置停用']]) { const option = node('option', title); option.value = value; select.append(option) }
|
|
303
|
+
select.addEventListener('change', () => { statusFilter = select.value; renderInstalled() })
|
|
304
|
+
filters.append(query, select)
|
|
155
305
|
const market = node('form', null, 'pc-market-tools'); market.hidden = true
|
|
156
306
|
const input = node('input', null, 'pc-search'); input.placeholder = '搜索插件或输入完整 npm 包名'; input.maxLength = 80; input.setAttribute('aria-label', '搜索插件或 npm 包名')
|
|
157
307
|
market.append(input, button('搜索', () => { offset = 0; return search() }), button('按包名查看', () => showDetails(input.value.trim())))
|
|
158
|
-
market.addEventListener('submit', event => {
|
|
159
|
-
|
|
308
|
+
market.addEventListener('submit', event => {
|
|
309
|
+
event.preventDefault(); offset = 0
|
|
310
|
+
const current = generation
|
|
311
|
+
search().catch(error => { if (current === generation) message(error.message) })
|
|
312
|
+
})
|
|
313
|
+
const jobs = node('details', null, 'pc-history'); jobs.append(node('summary', '操作记录'), node('div', null, 'pc-jobs'))
|
|
160
314
|
const alert = node('p', null, 'pc-message'); alert.setAttribute('role', 'status')
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
315
|
+
const context = node('div', null, 'pc-context'); context.append(node('p', null, 'pc-profile'), node('div', null, 'pc-summary'))
|
|
316
|
+
controls.append(toolbarRow, filters, market)
|
|
317
|
+
dialog.append(heading, context, node('p', null, 'pc-status'), controls, alert, node('div', null, 'pc-recovery'), node('div', null, 'pc-detail'), node('div', null, 'pc-list'), jobs)
|
|
318
|
+
const surface = dialog
|
|
319
|
+
dialog.addEventListener('close', () => {
|
|
320
|
+
if (dialog !== surface) return
|
|
321
|
+
generation++; clearTimeout(timer); dialog = null; surface.remove()
|
|
322
|
+
})
|
|
323
|
+
;(target || document.body).append(dialog)
|
|
164
324
|
}
|
|
165
|
-
generation++; searchSequence++; detailSequence++; connection = config; tab = 'installed'; pending = false; snapshot = null
|
|
325
|
+
generation++; searchSequence++; detailSequence++; connection = config; tab = 'installed'; pending = false; snapshot = null; filter = ''; statusFilter = 'all'
|
|
166
326
|
const current = generation
|
|
167
327
|
renderedInstalled = ''; renderedJobs = ''
|
|
168
328
|
dialog.querySelector('.pc-market-tools').hidden = true
|
|
169
329
|
dialog.querySelector('.pc-detail').replaceChildren(); dialog.querySelector('.pc-list').replaceChildren(); dialog.querySelector('.pc-jobs').replaceChildren()
|
|
170
|
-
|
|
330
|
+
if (target) dialog.open = true
|
|
331
|
+
else dialog.showModal()
|
|
332
|
+
message('正在读取插件状态…')
|
|
171
333
|
try { await refresh(); message('') } catch (error) { if (current === generation) message(error.message) }
|
|
172
334
|
if (current !== generation) return
|
|
173
335
|
async function poll() {
|
|
174
|
-
if (
|
|
336
|
+
if (current !== generation || !dialog?.open) return
|
|
175
337
|
try { await refresh() } catch (error) { if (current === generation) message(error.message) }
|
|
176
|
-
if (
|
|
338
|
+
if (current === generation && dialog?.open) timer = setTimeout(poll, 4000)
|
|
177
339
|
}
|
|
178
340
|
timer = setTimeout(poll, 4000)
|
|
179
341
|
}
|
|
180
|
-
return { open }
|
|
342
|
+
return { open, mount: (config, target) => open(config, target), close }
|
|
181
343
|
})()
|
package/public/plugin.js
CHANGED
|
@@ -10,7 +10,12 @@ let usageLoadedAt = 0
|
|
|
10
10
|
|
|
11
11
|
function text(id, value) { const el = $(id); if (el) el.textContent = value == null ? '—' : String(value) }
|
|
12
12
|
function escapeText(value) { return String(value ?? '').replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c])) }
|
|
13
|
-
function openConsole() {
|
|
13
|
+
function openConsole(event) {
|
|
14
|
+
event?.preventDefault()
|
|
15
|
+
// Desktop 拒绝 dsh-app: 弹窗;在当前 iframe 导航,沿用宿主认证与抽屉。
|
|
16
|
+
if (location.protocol === 'dsh-app:') location.assign('/remote/admin/')
|
|
17
|
+
else window.open('/remote/admin/', '_blank', 'noopener')
|
|
18
|
+
}
|
|
14
19
|
function closePanel() { window.parent.postMessage({ source: 'dsh-remote-plugin', type: 'close' }, location.origin) }
|
|
15
20
|
function fmtTokens(value) {
|
|
16
21
|
const n = Number(value) || 0
|
|
@@ -170,6 +175,7 @@ async function copyToken() {
|
|
|
170
175
|
|
|
171
176
|
$('plugin-close').addEventListener('click', closePanel)
|
|
172
177
|
$('plugin-console').addEventListener('click', openConsole)
|
|
178
|
+
if (location.protocol === 'dsh-app:') $('plugin-about').addEventListener('click', openConsole)
|
|
173
179
|
$('plugin-toggle').addEventListener('click', toggleGateway)
|
|
174
180
|
$('plugin-copy').addEventListener('click', copyToken)
|
|
175
181
|
$('plugin-primary').addEventListener('click', () => {
|