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.
@@ -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
- let renderedInstalled = '', renderedJobs = ''
6
- const labels = { install: '安装', update: '更新', remove: '卸载', enable: '启用', disable: '停用', queued: '等待执行', running: '执行中', complete: '已完成', failed: '失败' }
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
- el.append(node('h3', name), node('p', description))
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.items.length) list.append(node('p', '暂无可管理插件'))
44
- for (const item of snapshot.items) {
45
- const el = card(item.name, item.description)
46
- el.append(node('p', (item.version || item.requested || '内置') + ' · ' + (item.enabled ? '配置启用' : '配置停用') + (snapshot.pendingRestart ? ' · 待重启' : '')))
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
- const actions = node('div', null, 'pc-actions')
49
- actions.append(button('查看更新', () => showDetails(item.name)))
50
- if (item.bundle) actions.append(button(item.enabled ? '停用' : '启用', () => mutate(item.enabled ? 'disable' : 'enable', item.name)))
51
- actions.append(button('卸载', () => mutate('remove', item.name)))
52
- for (const action of actions.children) action.disabled = pending || snapshot.busy
53
- el.append(actions)
54
- } else el.append(node('small', '内置、核心或只读插件'))
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
- snapshot = await api('/state')
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
- const installedKey = JSON.stringify([snapshot.items, snapshot.runtime, snapshot.pendingRestart, snapshot.busy, snapshot.writable, pending])
68
- if (tab === 'installed' && installedKey !== renderedInstalled) { renderInstalled(); renderedInstalled = installedKey }
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
- el.append(node('small', item.version), button('查看详情', () => showDetails(item.name)))
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 sequence = ++detailSequence
103
- const { item } = await api('/details?name=' + encodeURIComponent(name) + (version ? '&version=' + encodeURIComponent(version) : ''))
104
- if (sequence !== detailSequence) return
105
- const area = dialog.querySelector('.pc-detail'); area.replaceChildren()
106
- const el = card(item.name + ' @ ' + item.version, item.description)
107
- el.append(node('p', '许可证:' + (item.license || '未声明') + ' · ' + (item.bundle ? 'DSH bundle' : '未声明 DSH bundle')))
108
- el.append(node('p', '运行要求:' + JSON.stringify(item.engines) + ';依赖要求:' + JSON.stringify(item.peers)))
109
- el.append(node('p', '插件代码将在 DSH 主机运行。安装脚本默认禁用;需要额外配置的插件须在主机完成配置。'))
110
- if (/^https:\/\//.test(item.homepage)) {
111
- const link = node('a', '项目主页'); link.href = item.homepage; link.target = '_blank'; link.rel = 'noopener noreferrer'; el.append(link)
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
- review.append(button('确认' + labels[action], () => executeMutation(body)), button('取消', () => area.replaceChildren()))
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('/operations', body)
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
- async function open(config) {
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'); heading.append(node('h2', '插件中心'), button('关闭', () => dialog.close()))
153
- const toolbar = node('div', null, 'pc-actions')
154
- toolbar.append(button('已安装', () => { tab = 'installed'; renderedInstalled = ''; dialog.querySelector('.pc-market-tools').hidden = true; return refresh() }), button('发现插件', () => { tab = 'market'; offset = 0; dialog.querySelector('.pc-market-tools').hidden = false; return search() }), button('刷新状态', refresh))
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 => { event.preventDefault(); offset = 0; search().catch(error => message(error.message)) })
159
- const jobs = node('details'); jobs.append(node('summary', '操作记录'), node('div', null, 'pc-jobs'))
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
- dialog.append(heading, node('p', null, 'pc-profile'), node('p', null, 'pc-status'), toolbar, market, alert, node('div', null, 'pc-detail'), node('div', null, 'pc-list'), jobs)
162
- dialog.addEventListener('close', () => { generation++; clearTimeout(timer) })
163
- document.body.append(dialog)
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
- dialog.showModal(); message('正在读取插件状态…')
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 (!dialog.open || current !== generation) return
336
+ if (current !== generation || !dialog?.open) return
175
337
  try { await refresh() } catch (error) { if (current === generation) message(error.message) }
176
- if (dialog.open && current === generation) timer = setTimeout(poll, 4000)
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 => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[c])) }
13
- function openConsole() { window.open('/remote/admin/', '_blank', 'noopener') }
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', () => {