dsh-update-center 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":[],"sources":["../src/client/index.ts"],"sourcesContent":["/**\n * dsh-update-center 设置页面板。\n *\n * 同源 API 提供状态、检查、更新、市场(安装/卸载/禁用)动作;React 组件只负责\n * 面板挂载,界面使用原生 DOM,避免把宿主的 React 运行时打进插件 bundle。\n * 市场数据来自 awesome-dsh-plugin.com 清单(服务端白名单拉取,本地快照兜底)。\n */\nimport { createElement, useEffect, useRef } from 'react'\nimport type { ReactNode } from 'react'\nimport type { SlotsService } from '@deepseek-ai/dsh-client-ui-slots'\n\ntype ClientContext = {\n slots: SlotsService\n}\n\ntype MessageKind = 'ok' | 'info' | 'err'\ntype BadgeKind = 'ok' | 'warn' | 'err' | 'muted' | 'update'\n\nexport const inject = ['slots']\n\nconst API = '/update-center/api'\n\nfunction el<K extends keyof HTMLElementTagNameMap>(tag: K, cls?: string, text?: string): HTMLElementTagNameMap[K] {\n const element = document.createElement(tag)\n if (cls) element.className = cls\n if (text !== void 0) element.textContent = text\n return element\n}\n\nconst styles = `\n.uc-page{font-family:inherit;font-size:13px;line-height:1.5;width:min(100%,920px);min-width:0;overflow:hidden;padding:8px 4px 24px;color:var(--dsw-alias-label-primary)}\n.uc-toolbar,.uc-section-head,.uc-row-main,.uc-meta,.uc-actions{display:flex;align-items:center}\n.uc-toolbar{justify-content:space-between;gap:12px;padding:4px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}\n.uc-toolbar h2{font-size:16px;line-height:1.3;margin:0;letter-spacing:0}\n.uc-summary{color:var(--dsw-alias-label-tertiary);font-size:12px}\n.uc-tabs{display:flex;gap:4px;padding:12px 0 0;border-bottom:1px solid var(--dsw-alias-border-l2)}\n.uc-tab{border:1px solid transparent;border-bottom:0;border-radius:6px 6px 0 0;padding:6px 14px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer}\n.uc-tab.active{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font-weight:600}\n.uc-section{padding:18px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}\n.uc-section-head{justify-content:space-between;gap:12px;margin-bottom:10px}\n.uc-section h3{font-size:13px;line-height:1.4;margin:0;letter-spacing:0}\n.uc-repo-row{display:grid;grid-template-columns:minmax(180px,1fr) auto;gap:16px;align-items:center}\n.uc-row-main{gap:8px;min-width:0;flex-wrap:wrap}\n.uc-version{font-weight:650}\n.uc-meta{gap:6px;color:var(--dsw-alias-label-tertiary);font-size:11px;min-width:0;flex-wrap:wrap}\n.uc-path{display:block;min-width:0;max-width:460px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.uc-actions{gap:8px;justify-content:flex-end;flex-wrap:wrap}\n.uc-btn{min-height:32px;border:1px solid transparent;border-radius:6px;padding:6px 12px;background:#2878d0;color:#fff;font:inherit;font-size:12px;cursor:pointer;white-space:nowrap}\n.uc-btn.secondary{background:transparent;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary)}\n.uc-btn.danger{background:transparent;border-color:rgba(210,58,58,.45);color:#d23a3a}\n.uc-btn:disabled{opacity:.45;cursor:not-allowed}\n.uc-badge,.uc-tag{display:inline-flex;align-items:center;min-height:20px;border-radius:4px;padding:1px 7px;font-size:10px;line-height:1.4;white-space:nowrap}\n.uc-badge.ok{background:rgba(39,174,96,.14);color:#28945a}\n.uc-badge.warn{background:rgba(224,166,22,.16);color:#a87800}\n.uc-badge.err{background:rgba(210,58,58,.13);color:#d23a3a}\n.uc-badge.muted{background:var(--dsw-alias-bg-multi-select);color:var(--dsw-alias-label-tertiary)}\n.uc-badge.update{background:rgba(40,120,208,.14);color:#2878d0}\n.uc-list{list-style:none;margin:0;padding:0}\n.uc-item{display:grid;grid-template-columns:minmax(220px,1fr) minmax(130px,auto) auto;gap:12px;align-items:center;padding:11px 0;border-top:1px solid var(--dsw-alias-border-l2)}\n.uc-item>*{min-width:0}\n.uc-item:first-child{border-top:0}\n.uc-name{font-weight:600;overflow-wrap:anywhere}\n.uc-desc{color:var(--dsw-alias-label-secondary);font-size:12px;margin-top:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n.uc-tags{display:flex;gap:5px;align-items:center;flex-wrap:wrap;margin-top:3px}\n.uc-tag{background:var(--dsw-alias-bg-multi-select);color:var(--dsw-alias-label-secondary)}\n.uc-tag.npm{background:rgba(40,120,208,.12);color:#2878d0}\n.uc-tag.link{background:rgba(39,174,96,.12);color:#28945a}\n.uc-tag.preset{background:rgba(151,89,182,.13);color:#9858b4}\n.uc-status{display:flex;gap:6px;align-items:center;justify-content:flex-end;flex-wrap:wrap}\n.uc-empty{padding:14px 0;color:var(--dsw-alias-label-tertiary)}\n.uc-market-head{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:14px 0}\n.uc-input,.uc-select{min-height:32px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;padding:6px 10px}\n.uc-input{flex:1 1 200px;min-width:160px}\n.uc-select{flex:0 0 auto}\n.uc-market-meta{width:100%;color:var(--dsw-alias-label-tertiary);font-size:11px}\n.uc-msg{margin-top:14px;padding:10px 12px;border-left:3px solid var(--dsw-alias-border-l3);background:var(--dsw-alias-bg-layer-2);white-space:pre-wrap;max-height:260px;overflow:auto;font-size:12px}\n.uc-msg.ok{border-color:#28945a}\n.uc-msg.err{border-color:#d23a3a}\n.uc-msg.info{border-color:#2878d0}\n@media(max-width:680px){\n [class$=\"_panel\"]:has(.uc-page)>nav{width:52px!important;flex-basis:52px!important;padding-inline:6px!important}\n [class$=\"_panel\"]:has(.uc-page)>nav [class$=\"_navTitle\"]{display:none!important}\n [class$=\"_panel\"]:has(.uc-page)>nav [class$=\"_navCell\"]{width:40px!important;min-width:40px!important;justify-content:center!important;padding-inline:0!important}\n [class$=\"_panel\"]:has(.uc-page)>nav [class$=\"_navLabel\"]{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip-path:inset(50%)!important;white-space:nowrap!important;border:0!important}\n .uc-toolbar{align-items:flex-start;flex-direction:column}\n .uc-section-head{align-items:flex-start;flex-wrap:wrap}\n .uc-repo-row,.uc-item{grid-template-columns:1fr}\n .uc-actions,.uc-status{justify-content:flex-start}\n .uc-path{width:100%;max-width:100%}\n}\n`\n\nasync function fetchJson(path: string, init?: RequestInit): Promise<any> {\n const response = await fetch(API + path, {\n headers: { 'content-type': 'application/json' },\n ...init,\n })\n const data = await response.json().catch(() => ({ ok: false, error: `HTTP ${response.status}` }))\n if (!response.ok) throw new Error(data?.error || `HTTP ${response.status}`)\n return data\n}\n\nfunction buildPanel(): HTMLElement {\n const style = el('style')\n style.textContent = styles\n\n const page = el('div', 'uc-page')\n page.append(style)\n\n const toolbar = el('div', 'uc-toolbar')\n const heading = el('div')\n heading.append(el('h2', undefined, '更新中心'))\n const summary = el('div', 'uc-summary', '正在读取本地状态…')\n heading.append(summary)\n const checkButton = el('button', 'uc-btn secondary', '检查更新')\n checkButton.type = 'button'\n const updateAllButton = el('button', 'uc-btn', '全部更新')\n updateAllButton.type = 'button'\n updateAllButton.disabled = true\n const toolbarActions = el('div', 'uc-actions')\n toolbarActions.append(checkButton, updateAllButton)\n toolbar.append(heading, toolbarActions)\n\n // ── Tab 切换:已安装 / 插件市场 ──\n const tabs = el('div', 'uc-tabs')\n const installedTab = el('button', 'uc-tab active', '已安装')\n installedTab.type = 'button'\n const marketTab = el('button', 'uc-tab', '插件市场')\n marketTab.type = 'button'\n tabs.append(installedTab, marketTab)\n\n const installedView = el('div')\n\n const repoSection = el('section', 'uc-section')\n const repoHead = el('div', 'uc-section-head')\n repoHead.append(el('h3', undefined, 'dsh 本体'))\n const repoBadge = badge('未检查', 'muted')\n repoHead.append(repoBadge)\n const repoContent = el('div')\n repoSection.append(repoHead, repoContent)\n\n const pluginSection = el('section', 'uc-section')\n const pluginHead = el('div', 'uc-section-head')\n pluginHead.append(el('h3', undefined, '已安装插件'))\n const pluginCount = badge('0 个', 'muted')\n pluginHead.append(pluginCount)\n const pluginList = el('ul', 'uc-list')\n pluginSection.append(pluginHead, pluginList)\n installedView.append(repoSection, pluginSection)\n\n // ── 市场视图 ──\n const marketView = el('div')\n marketView.style.display = 'none'\n const marketSection = el('section', 'uc-section')\n const marketHead = el('div', 'uc-market-head')\n const searchInput = el('input', 'uc-input')\n searchInput.type = 'search'\n searchInput.placeholder = '搜索插件名 / 作者 / 描述…'\n const categorySelect = el('select', 'uc-select')\n const sortSelect = el('select', 'uc-select')\n const sortStars = el('option', undefined, '星标最多')\n sortStars.value = 'stars'\n const sortDefault = el('option', undefined, '默认顺序')\n sortDefault.value = 'default'\n const sortAdded = el('option', undefined, '最近添加')\n sortAdded.value = 'added'\n sortSelect.append(sortStars, sortDefault, sortAdded)\n sortSelect.value = 'stars'\n const refreshButton = el('button', 'uc-btn secondary', '刷新清单')\n refreshButton.type = 'button'\n const marketMeta = el('div', 'uc-market-meta', '尚未加载市场清单')\n marketHead.append(searchInput, categorySelect, sortSelect, refreshButton, marketMeta)\n const marketList = el('ul', 'uc-list')\n marketSection.append(marketHead, marketList)\n marketView.append(marketSection)\n\n const message = el('div', 'uc-msg')\n message.style.display = 'none'\n page.append(toolbar, tabs, installedView, marketView, message)\n\n const buttons = new Set<HTMLButtonElement>([checkButton, updateAllButton])\n let checked = false\n let activeJobId = ''\n let marketData: any = null\n let marketQuery = ''\n let marketCategory = ''\n let marketSort = 'stars'\n let lastStatusData: any = null\n\n /** 已安装插件里有多少项可更新(用于“全部更新”按钮态)。 */\n function pluginUpdateCount(plugins: any[]): number {\n return plugins.filter((plugin) => {\n const git = plugin.git ?? {}\n return (plugin.kind === 'npm' && plugin.latest && plugin.latest !== plugin.version)\n || ((plugin.kind === 'link' || plugin.kind === 'preset') && Number(git.behind) > 0 && !git.dirty)\n }).length\n }\n\n /** 市场清单里与某个已安装依赖对应的条目(npm 名或仓库名匹配)。 */\n function marketEntryFor(name: string): any | null {\n const plugins: any[] = Array.isArray(marketData?.plugins) ? marketData.plugins : []\n return plugins.find((entry) => entry.name === name || (entry.npm && entry.npm === name)) ?? null\n }\n\n function externalLink(text: string, url: string): HTMLAnchorElement {\n const link = el('a', 'uc-meta', text)\n link.href = url\n link.target = '_blank'\n link.rel = 'noreferrer'\n return link\n }\n\n function badge(text: string, kind: BadgeKind): HTMLSpanElement {\n return el('span', 'uc-badge ' + kind, text)\n }\n\n function say(text: string, kind: MessageKind = 'info'): void {\n message.textContent = text\n message.style.display = text ? 'block' : 'none'\n message.className = 'uc-msg ' + kind\n }\n\n function setBusy(active: boolean, activeButton?: HTMLButtonElement, label?: string): void {\n for (const button of buttons) {\n if (!button.isConnected) {\n buttons.delete(button)\n continue\n }\n if (active) {\n button.dataset.disabledBeforeBusy = button.disabled ? '1' : '0'\n button.disabled = true\n } else if (button.dataset.disabledBeforeBusy) {\n button.disabled = button.dataset.disabledBeforeBusy === '1'\n delete button.dataset.disabledBeforeBusy\n }\n }\n if (!activeButton) return\n if (active) {\n activeButton.dataset.originalLabel = activeButton.textContent ?? ''\n if (label) activeButton.textContent = label\n } else if (activeButton.dataset.originalLabel) {\n activeButton.textContent = activeButton.dataset.originalLabel\n delete activeButton.dataset.originalLabel\n }\n }\n\n function actionButton(label: string, variant: 'primary' | 'secondary' | 'danger' = 'primary'): HTMLButtonElement {\n const cls = variant === 'primary' ? 'uc-btn' : `uc-btn ${variant}`\n const button = el('button', cls, label)\n button.type = 'button'\n buttons.add(button)\n return button\n }\n\n function updateSummary(data: any): void {\n if (data?.job?.status === 'queued' || data?.job?.status === 'running') {\n summary.textContent = `后台更新中 · ${String(data.job.message || data.job.stage || '')}`\n return\n }\n if (!checked) {\n summary.textContent = '本地状态已载入,点击“检查更新”获取远端状态'\n return\n }\n const total = Number(data?.summary?.totalUpdates ?? 0)\n const time = data?.ts ? new Date(data.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''\n summary.textContent = total > 0 ? `发现 ${total} 项可更新 · ${time}` : `全部为最新 · ${time}`\n }\n\n function jobText(job: any): string {\n const lines = [String(job?.message || '更新任务')]\n if (Array.isArray(job?.steps)) lines.push(...job.steps.map(String))\n if (job?.error) lines.push(String(job.error))\n return lines.filter(Boolean).join('\\n')\n }\n\n function pollJob(id: string, activeButton?: HTMLButtonElement): void {\n activeJobId = id\n fetchJson('/job?id=' + encodeURIComponent(id))\n .then((data) => {\n const job = data?.job\n if (!job) throw new Error('更新任务不存在')\n if (job.status === 'queued' || job.status === 'running') {\n say(jobText(job), 'info')\n if (page.isConnected) window.setTimeout(() => pollJob(id, activeButton), 1000)\n return\n }\n activeJobId = ''\n checked = false\n return refreshStatus(false, true).then(() => {\n if (marketData) void loadMarket(false, true)\n setBusy(false, activeButton)\n if (job.status === 'completed') {\n const suffix = job.restartRequired ? '\\n\\n更新已核验完成,现在可以重启 dsh web。' : ''\n say(jobText(job) + suffix, 'ok')\n } else {\n say(jobText(job), 'err')\n }\n })\n })\n .catch((error) => {\n if (!page.isConnected) return\n say('暂时无法读取更新进度,dsh 重启后会从磁盘恢复任务状态:' + String(error), 'info')\n window.setTimeout(() => pollJob(id, activeButton), 1500)\n })\n }\n\n function resumeJob(job: any): void {\n if (!job?.id || activeJobId === job.id) return\n if (job.status === 'queued' || job.status === 'running') {\n setBusy(true)\n pollJob(String(job.id))\n }\n }\n\n /** 统一动作绑定:POST 后要么直接返回结果(disable/enable),要么轮询 job。 */\n function bindAction(button: HTMLButtonElement, path: string, body: Record<string, unknown>, confirmation: string, startingNote: string): void {\n button.addEventListener('click', () => {\n if (!window.confirm(confirmation)) return\n setBusy(true, button, '处理中…')\n say(startingNote, 'info')\n fetchJson(path, { method: 'POST', body: JSON.stringify(body) })\n .then((data) => {\n if (!data?.ok) throw new Error(data?.result || data?.error || '操作失败')\n const id = String(data?.job?.id || '')\n if (!id) {\n setBusy(false, button)\n say(String(data?.result || '操作成功'), 'ok')\n return refreshStatus(false, true).then(() => {\n if (marketData) void loadMarket(false, true)\n })\n }\n button.textContent = '后台执行中…'\n pollJob(id, button)\n })\n .catch((error) => {\n setBusy(false, button)\n say('请求失败:' + String(error), 'err')\n })\n })\n }\n\n function renderRepo(repo: any): void {\n repoContent.textContent = ''\n const git = repo?.git ?? {}\n const row = el('div', 'uc-repo-row')\n const info = el('div')\n const main = el('div', 'uc-row-main')\n main.append(el('span', 'uc-version', String(repo?.version || '未知版本')))\n if (git.head) main.append(el('span', 'uc-meta', String(git.head)))\n if (git.branch) main.append(el('span', 'uc-tag', String(git.branch)))\n info.append(main)\n const meta = el('div', 'uc-meta')\n if (repo?.path) meta.append(el('span', 'uc-path', String(repo.path)))\n if (git.ahead > 0) meta.append(el('span', undefined, `本地领先 ${git.ahead}`))\n info.append(meta)\n\n repoBadge.className = 'uc-badge muted'\n repoBadge.textContent = checked ? '状态未知' : '未检查'\n if (repo?.error) {\n repoBadge.className = 'uc-badge err'\n repoBadge.textContent = '仓库不可用'\n } else if (git.dirty) {\n repoBadge.className = 'uc-badge warn'\n repoBadge.textContent = '有未提交改动'\n } else if (checked && repo?.fetchOk === false) {\n repoBadge.className = 'uc-badge err'\n repoBadge.textContent = '检查失败'\n } else if (checked && git.behind > 0) {\n repoBadge.className = 'uc-badge update'\n repoBadge.textContent = `落后 ${git.behind} 个提交`\n } else if (checked && typeof git.behind === 'number') {\n repoBadge.className = 'uc-badge ok'\n repoBadge.textContent = '已是最新'\n }\n\n const actions = el('div', 'uc-actions')\n const fullButton = actionButton('完整更新')\n const pullButton = actionButton('仅拉取代码', 'secondary')\n const canUpdate = checked && !repo?.error && !git.dirty && Number(git.behind) > 0\n fullButton.disabled = !canUpdate\n pullButton.disabled = !canUpdate\n bindAction(fullButton, '/update-dsh', { full: true }, '完整更新会在独立后台任务中拉取代码、安装依赖并重新构建 dsh。任务完成后再重启,是否继续?', '正在创建后台更新任务…')\n bindAction(pullButton, '/update-dsh', { full: false }, '仅拉取代码不会安装依赖或重新构建,重启后仍可能运行旧产物。确认继续?', '正在创建后台更新任务…')\n actions.append(fullButton, pullButton)\n row.append(info, actions)\n repoContent.append(row)\n\n if (git.dirty && Array.isArray(git.dirtyFiles) && git.dirtyFiles.length) {\n const dirty = el('div', 'uc-msg info', `为保护现有工作,更新已禁用:\\n${git.dirtyFiles.join('\\n')}`)\n repoContent.append(dirty)\n }\n }\n\n function renderPlugins(plugins: any[]): void {\n pluginList.textContent = ''\n pluginCount.textContent = `${plugins.length} 个`\n if (!plugins.length) {\n pluginList.append(el('li', 'uc-empty', '当前 profile 没有外部插件,去“插件市场”看看?'))\n return\n }\n\n for (const plugin of plugins) {\n const item = el('li', 'uc-item')\n const identity = el('div')\n identity.append(el('div', 'uc-name', String(plugin.name)))\n const marketEntry = marketEntryFor(String(plugin.name))\n const description = marketEntry?.description?.zh || marketEntry?.description?.en\n if (description) identity.append(el('div', 'uc-desc', String(description)))\n const tags = el('div', 'uc-tags')\n const kind = plugin.kind === 'preset' ? 'preset' : plugin.kind === 'link' ? 'link' : 'npm'\n tags.append(el('span', 'uc-tag ' + kind, kind))\n if (plugin.bundled) tags.append(el('span', 'uc-tag', 'bundle'))\n if (plugin.upstream && String(plugin.upstream).includes('/')) {\n tags.append(externalLink(String(plugin.upstream), `https://github.com/${plugin.upstream}`))\n } else if (plugin.upstream) {\n tags.append(el('span', 'uc-meta', String(plugin.upstream)))\n }\n if (plugin.linkDir) tags.append(el('span', 'uc-meta uc-path', String(plugin.linkDir)))\n identity.append(tags)\n\n const status = el('div', 'uc-status')\n status.append(el('span', 'uc-meta', plugin.version ? `v${plugin.version}` : '版本未知'))\n const git = plugin.git ?? {}\n let updateAvailable = false\n let blocked = false\n if (plugin.disabled) {\n status.append(badge('已禁用', 'muted'))\n }\n if ((kind === 'link' || kind === 'preset') && git.dirty) {\n status.append(badge('有未提交改动', 'warn'))\n blocked = true\n } else if ((kind === 'link' || kind === 'preset') && checked && git.fetchOk === false) {\n status.append(badge('检查失败', 'err'))\n } else if ((kind === 'link' || kind === 'preset') && checked && Number(git.behind) > 0) {\n status.append(badge(`落后 ${git.behind}`, 'update'))\n updateAvailable = true\n } else if (kind === 'npm' && checked && plugin.latest && plugin.latest !== plugin.version) {\n status.append(badge(`可更新至 ${plugin.latest}`, 'update'))\n updateAvailable = true\n } else if (checked && ((kind === 'npm' && plugin.latest) || typeof git.behind === 'number')) {\n status.append(badge('已是最新', 'ok'))\n } else if (!plugin.disabled) {\n status.append(badge('未检查', 'muted'))\n }\n\n const actions = el('div', 'uc-actions')\n const updateButton = actionButton('更新')\n updateButton.disabled = !updateAvailable || blocked\n if (kind === 'npm') {\n bindAction(updateButton, '/update-npm', { name: plugin.name }, `后台更新 ${plugin.name} 到最新版,并核对实际安装版本。任务完成后再重启,是否继续?`, '正在创建后台更新任务…')\n } else {\n bindAction(updateButton, '/update-link', { name: plugin.name }, `后台更新本地插件 ${plugin.name};若只更新源码,会自动重新构建运行产物。是否继续?`, '正在创建后台更新任务…')\n }\n actions.append(updateButton)\n if (kind === 'npm') {\n const toggleButton = actionButton(plugin.disabled ? '启用' : '禁用', 'secondary')\n bindAction(toggleButton, plugin.disabled ? '/enable' : '/disable', { name: plugin.name },\n plugin.disabled ? `恢复启用 ${plugin.name}?重启 dsh web 后生效。` : `禁用 ${plugin.name}?只是写入配置不卸载,重启 dsh web 后生效。`,\n '正在更新配置…')\n actions.append(toggleButton)\n const removeButton = actionButton('卸载', 'danger')\n bindAction(removeButton, '/uninstall', { name: plugin.name }, `从 profile 卸载 ${plugin.name}(pnpm remove + 移出 bundles)。已禁用状态会一并清理。任务完成后再重启,是否继续?`, '正在创建后台卸载任务…')\n actions.append(removeButton)\n }\n item.append(identity, status, actions)\n pluginList.append(item)\n }\n }\n\n // ── 市场渲染 ──\n function renderCategorySelect(): void {\n const categories = marketData?.categories ?? {}\n const previous = marketCategory\n categorySelect.textContent = ''\n const all = el('option', undefined, '全部分类')\n all.value = ''\n categorySelect.append(all)\n for (const [key, label] of Object.entries<any>(categories)) {\n const option = el('option', undefined, String(label?.zh || label?.en || key))\n option.value = key\n categorySelect.append(option)\n }\n marketCategory = previous && [...categorySelect.options].some((o) => o.value === previous) ? previous : ''\n categorySelect.value = marketCategory\n }\n\n function renderMarket(): void {\n marketList.textContent = ''\n if (!marketData) {\n marketList.append(el('li', 'uc-empty', '清单尚未加载'))\n return\n }\n const plugins: any[] = Array.isArray(marketData.plugins) ? marketData.plugins : []\n const installedSet = new Set<string>(Array.isArray(marketData.installed) ? marketData.installed.map(String) : [])\n const query = marketQuery.trim().toLowerCase()\n const filtered = plugins.filter((entry) => {\n if (marketCategory && entry.category !== marketCategory) return false\n if (!query) return true\n const haystack = [entry.name, entry.owner, entry.npm, entry.description?.zh, entry.description?.en]\n .filter(Boolean).join(' ').toLowerCase()\n return haystack.includes(query)\n })\n // 排序:星标最多 / 最近添加;同值保持清单原顺序(sort 稳定)。\n if (marketSort === 'stars') {\n filtered.sort((a, b) => Number(b.stars ?? 0) - Number(a.stars ?? 0))\n } else if (marketSort === 'added') {\n filtered.sort((a, b) => (Date.parse(String(b.added ?? '')) || 0) - (Date.parse(String(a.added ?? '')) || 0))\n }\n const sourceLabel = { registry: '本仓库 plugins.json', github: 'GitHub topic:dsh-plugin', disk: '本地缓存', snapshot: '内置快照', network: '网络' }[String(marketData.source)] ?? marketData.source\n marketMeta.textContent = `来源:${sourceLabel} · 扫描于 ${String(marketData.updated || '未知')} · 共 ${plugins.length} 个,筛选出 ${filtered.length} 个${filtered.length > 300 ? '(仅显示前 300)' : ''}`\n if (!filtered.length) {\n marketList.append(el('li', 'uc-empty', '没有匹配的插件'))\n return\n }\n for (const entry of filtered.slice(0, 300)) {\n const installed = installedSet.has(String(entry.name)) || (entry.npm && installedSet.has(String(entry.npm)))\n const item = el('li', 'uc-item')\n const identity = el('div')\n const nameRow = el('div', 'uc-name')\n if (entry.url) {\n nameRow.append(externalLink(String(entry.name), String(entry.url)))\n } else {\n nameRow.textContent = String(entry.name)\n }\n identity.append(nameRow)\n const description = entry.description?.zh || entry.description?.en || ''\n if (description) identity.append(el('div', 'uc-desc', String(description)))\n const tags = el('div', 'uc-tags')\n if (entry.owner) tags.append(el('span', 'uc-meta', String(entry.owner)))\n if (entry.npm) tags.append(el('span', 'uc-tag npm', 'npm'))\n else tags.append(el('span', 'uc-tag', 'github'))\n identity.append(tags)\n\n const status = el('div', 'uc-status')\n const stars = Number(entry.stars ?? 0)\n status.append(el('span', 'uc-meta', stars > 0 ? `★ ${stars}` : '暂无星标'))\n const statusPlugin = (lastStatusData?.plugins ?? []).find((plugin: any) =>\n plugin.name === entry.npm || plugin.name === entry.name)\n const updatable = statusPlugin\n && ((statusPlugin.kind === 'npm' && statusPlugin.latest && statusPlugin.latest !== statusPlugin.version)\n || Number(statusPlugin.git?.behind) > 0)\n\n const actions = el('div', 'uc-actions')\n if (installed) {\n status.append(updatable ? badge('可更新', 'update') : badge('已安装', 'ok'))\n const removeButton = actionButton('卸载', 'danger')\n bindAction(removeButton, '/uninstall', { name: entry.npm || entry.name }, `从 profile 卸载 ${entry.npm || entry.name}?任务完成后再重启,是否继续?`, '正在创建后台卸载任务…')\n actions.append(removeButton)\n } else {\n const installButton = actionButton('安装')\n bindAction(installButton, '/install', { name: entry.name }, `在后台安装 ${entry.name}(优先 npm,来源不一致时回退 GitHub)。安装完成后需要重启 dsh web,是否继续?`, '正在创建后台安装任务…')\n actions.append(installButton)\n }\n item.append(identity, status, actions)\n marketList.append(item)\n }\n }\n\n async function loadMarket(force: boolean, silent: boolean): Promise<void> {\n if (force) setBusy(true, refreshButton, '刷新中…')\n if (!silent) say('正在加载插件市场清单…', 'info')\n try {\n const data = await fetchJson(force ? '/market/refresh' : '/market')\n if (!data?.ok) throw new Error(data?.error || '清单加载失败')\n marketData = data\n renderCategorySelect()\n renderMarket()\n if (lastStatusData) renderPlugins(lastStatusData.plugins ?? [])\n if (!silent) say('', 'info')\n } catch (error) {\n if (!silent) say('市场清单加载失败:' + String(error), 'err')\n } finally {\n if (force) setBusy(false, refreshButton)\n }\n }\n\n function switchTab(target: 'installed' | 'market'): void {\n const showMarket = target === 'market'\n installedTab.className = showMarket ? 'uc-tab' : 'uc-tab active'\n marketTab.className = showMarket ? 'uc-tab active' : 'uc-tab'\n installedView.style.display = showMarket ? 'none' : 'block'\n marketView.style.display = showMarket ? 'block' : 'none'\n if (showMarket && !marketData) void loadMarket(false, false)\n }\n\n function render(data: any): void {\n lastStatusData = data\n renderRepo(data?.repo)\n renderPlugins(Array.isArray(data?.plugins) ? data.plugins : [])\n updateSummary(data)\n updateAllButton.disabled = !checked || pluginUpdateCount(data?.plugins ?? []) === 0\n }\n\n function refreshStatus(showJob = true, fresh = false): Promise<void> {\n return fetchJson(fresh ? '/status?fresh=1' : '/status')\n .then((data) => {\n if (!data?.ok) throw new Error(data?.error || '状态读取失败')\n render(data)\n if (showJob) {\n resumeJob(data.job)\n if (data?.job?.status === 'completed') say(jobText(data.job), 'ok')\n if (data?.job?.status === 'failed') say(jobText(data.job), 'err')\n }\n })\n .catch((error) => say('状态读取失败:' + String(error), 'err'))\n }\n\n installedTab.addEventListener('click', () => switchTab('installed'))\n marketTab.addEventListener('click', () => switchTab('market'))\n searchInput.addEventListener('input', () => {\n marketQuery = searchInput.value\n renderMarket()\n })\n categorySelect.addEventListener('change', () => {\n marketCategory = categorySelect.value\n renderMarket()\n })\n sortSelect.addEventListener('change', () => {\n marketSort = sortSelect.value\n renderMarket()\n })\n refreshButton.addEventListener('click', () => void loadMarket(true, false))\n bindAction(updateAllButton, '/update-all', {}, '把所有可更新的插件(不含 dsh 本体)排成一个后台批量任务串行执行,单个失败不影响其余。任务完成后再重启,是否继续?', '正在检查并创建批量更新任务…')\n\n checkButton.addEventListener('click', () => {\n setBusy(true, checkButton, '检查中…')\n say('正在检查 dsh 和插件的远端状态…', 'info')\n fetchJson('/check', { method: 'POST', body: '{}' })\n .then((data) => {\n if (!data?.ok) throw new Error(data?.error || '检查失败')\n checked = true\n render(data)\n if (marketData) renderMarket()\n const total = Number(data?.summary?.totalUpdates ?? 0)\n const failed = Number(data?.summary?.failedChecks ?? 0)\n if (failed > 0) {\n say(`检查完成,发现 ${total} 项可更新,另有 ${failed} 项检查失败。`, 'err')\n } else {\n say(total > 0 ? `检查完成,发现 ${total} 项可更新。` : '检查完成,当前全部为最新。', total > 0 ? 'info' : 'ok')\n }\n })\n .catch((error) => say('检查失败:' + String(error), 'err'))\n .finally(() => setBusy(false, checkButton))\n })\n\n void refreshStatus()\n return page\n}\n\nfunction UpdateCenterPanel(): ReactNode {\n const hostRef = useRef<HTMLDivElement | null>(null)\n useEffect(() => {\n const host = hostRef.current\n if (!host) return\n const panel = buildPanel()\n host.appendChild(panel)\n return () => { panel.remove() }\n }, [])\n return createElement('div', { ref: hostRef })\n}\n\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.slots.inject('settings.section', () =>\n ctx.slots.register({\n name: 'settings.section',\n id: 'update-center',\n order: 60,\n label: () => '更新中心',\n }, UpdateCenterPanel),\n ), 'dsh-update-center: panel')\n}\n"],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * GitHub topic 扫描的条目转换、清单构建与合并(纯函数,供服务端与测试复用)。
3
+ *
4
+ * 市场数据源为 GitHub 上带 dsh-plugin 话题的仓库(按星标降序取前若干);
5
+ * 扫描结果构造成 registry 形状,常驻条目按 owner/name 去重并入。
6
+ */
7
+
8
+ export function githubRepoToEntry(repo) {
9
+ const owner = repo?.owner?.login ?? ''
10
+ const name = repo?.name ?? ''
11
+ const description = typeof repo?.description === 'string' ? repo.description : ''
12
+ return {
13
+ name,
14
+ owner,
15
+ url: typeof repo?.html_url === 'string' ? repo.html_url : `https://github.com/${owner}/${name}`,
16
+ page: '',
17
+ category: 'github',
18
+ description: { en: description, zh: '' },
19
+ npm: null,
20
+ stars: Number(repo?.stargazers_count ?? 0),
21
+ install: owner && name ? `dsh plugin --profile web add github:${owner}/${name}` : '',
22
+ added: typeof repo?.pushed_at === 'string' ? repo.pushed_at.slice(0, 10) : '',
23
+ }
24
+ }
25
+
26
+ /**
27
+ * 把 GitHub 扫描结果合并进 registry 数据(原地修改)。
28
+ * 匹配键为 owner/name(忽略大小写);已知条目只更新星标,新条目追加并标记
29
+ * github 分类。返回 { added, starsUpdated }。
30
+ */
31
+ export function mergeGithubTopic(data, repos) {
32
+ if (!data || !Array.isArray(data.plugins) || !Array.isArray(repos)) return { added: 0, starsUpdated: 0 }
33
+ const index = new Map(data.plugins.map((plugin) => [
34
+ `${String(plugin.owner ?? '').toLowerCase()}/${String(plugin.name ?? '').toLowerCase()}`,
35
+ plugin,
36
+ ]))
37
+ let added = 0
38
+ let starsUpdated = 0
39
+ for (const repo of repos) {
40
+ const entry = githubRepoToEntry(repo)
41
+ if (!entry.owner || !entry.name || !entry.install) continue
42
+ const key = `${entry.owner.toLowerCase()}/${entry.name.toLowerCase()}`
43
+ const known = index.get(key)
44
+ if (known) {
45
+ if (entry.stars > Number(known.stars ?? 0)) {
46
+ known.stars = entry.stars
47
+ starsUpdated += 1
48
+ }
49
+ continue
50
+ }
51
+ data.plugins.push(entry)
52
+ index.set(key, entry)
53
+ added += 1
54
+ }
55
+ if (added > 0) {
56
+ const categories = { ...(data.categories ?? {}) }
57
+ if (!categories.github) categories.github = { en: 'GitHub discovered', zh: 'GitHub 发现' }
58
+ data.categories = categories
59
+ }
60
+ data.githubExtra = added
61
+ return { added, starsUpdated }
62
+ }
63
+
64
+ /** 把 GitHub 扫描结果构建成完整 registry 形状(单分类 GitHub 发现,按星标降序)。 */
65
+ export function buildRegistryFromRepos(repos) {
66
+ const entries = (Array.isArray(repos) ? repos : [])
67
+ .filter((repo) => repo && repo.fork !== true)
68
+ .map(githubRepoToEntry)
69
+ .filter((entry) => entry.owner && entry.name && entry.install)
70
+ .sort((a, b) => Number(b.stars ?? 0) - Number(a.stars ?? 0))
71
+ return {
72
+ name: 'dsh-plugin-topic',
73
+ url: 'https://github.com/search?q=topic%3Adsh-plugin',
74
+ source: 'GitHub topic:dsh-plugin',
75
+ updated: new Date().toISOString().slice(0, 10),
76
+ count: entries.length,
77
+ categories: { github: { en: 'GitHub discovered', zh: 'GitHub 发现' } },
78
+ plugins: entries,
79
+ }
80
+ }