@huaqiu/dsh-tool-plugin-update 0.0.0-stage → 0.5.21
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/LICENSE +21 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +1006 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +32 -0
- package/lib/index.mjs +178 -0
- package/package.json +50 -3
- package/src/client/api.ts +94 -0
- package/src/client/copy.ts +92 -0
- package/src/client/index.ts +508 -0
- package/src/client/update-view.ts +301 -0
- package/src/index.ts +108 -0
- package/src/routes.ts +149 -0
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":[],"sources":["../../dsh-sidebar-entry/lib/index.mjs","../src/client/api.ts","../src/client/copy.ts","../src/client/update-view.ts","../src/client/index.ts"],"sourcesContent":["//#region src/index.ts\n/** Find the sidebar shell root element, or undefined while not yet mounted. */\nfunction sidebarRoot() {\n\tconst column = document.querySelector(\"[data-pane=\\\"sidebar\\\"], [class*=\\\"sidebarCol\\\"]\");\n\tif (column === null) return void 0;\n\treturn column.querySelector(\"[class*=\\\"logoRow\\\"]\")?.parentElement ?? column.firstElementChild;\n}\n/** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */\nfunction newSessionButton(root) {\n\tconst nested = root.querySelector(\"button[class*=\\\"newSession\\\"]\");\n\tif (nested !== null) return nested;\n\tfor (const child of root.children) if (child.tagName === \"BUTTON\") return child;\n}\n/** Build the entry row (a detached button; insert once the shell is up). */\nfunction createEntry(options) {\n\tconst entry = document.createElement(\"button\");\n\tentry.type = \"button\";\n\tentry.setAttribute(options.rowAttribute, \"\");\n\tif (options.plugin !== void 0) {\n\t\tentry.setAttribute(\"data-dsh-plugin\", options.plugin);\n\t\tentry.setAttribute(\"data-dsh-part\", \"sidebar-entry\");\n\t}\n\tentry.className = options.css[\"entry\"] ?? \"\";\n\tconst labelSpan = document.createElement(\"span\");\n\tlabelSpan.className = options.css[\"entryLabel\"] ?? \"\";\n\tconst iconSpan = document.createElement(\"span\");\n\ticonSpan.className = options.css[\"entryIcon\"] ?? \"\";\n\ticonSpan.innerHTML = options.icon;\n\tentry.append(iconSpan, labelSpan);\n\tlet badgeSpan = null;\n\tconst applyBadge = () => {\n\t\tif (options.badge === void 0) return;\n\t\tconst count = options.badge.count();\n\t\tif (count <= 0) {\n\t\t\tbadgeSpan?.remove();\n\t\t\tbadgeSpan = null;\n\t\t\treturn;\n\t\t}\n\t\tif (badgeSpan === null) {\n\t\t\tbadgeSpan = document.createElement(\"span\");\n\t\t\tbadgeSpan.className = options.css[\"entryBadge\"] ?? \"\";\n\t\t\tentry.append(badgeSpan);\n\t\t}\n\t\tbadgeSpan.textContent = String(count);\n\t\tentry.setAttribute(\"data-has-badge\", \"\");\n\t};\n\tconst applyLabel = () => {\n\t\tentry.setAttribute(\"aria-label\", options.label());\n\t\tif (options.tooltip !== void 0) entry.setAttribute(\"title\", options.tooltip());\n\t\tlabelSpan.textContent = options.label();\n\t};\n\tapplyLabel();\n\tapplyBadge();\n\tentry.addEventListener(\"click\", options.onToggle);\n\treturn {\n\t\tentry,\n\t\tapplyLabel,\n\t\tapplyBadge\n\t};\n}\n/** Re-insert the entry after the New Session row (before the browser region). */\nfunction placeEntry(root, entry, options) {\n\tconst button = newSessionButton(root);\n\tif (button === void 0) return false;\n\tif (entry.parentElement !== root) {\n\t\tconst row = button.closest(\"[class*=\\\"logoRow\\\"]\");\n\t\tconst base = row !== null && row.parentElement === root ? row : button;\n\t\tconst family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(\", \")));\n\t\tconst anchor = options.position === \"before\" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;\n\t\troot.insertBefore(entry, anchor ?? null);\n\t}\n\treturn true;\n}\n/**\n* Mount the sidebar entry, waiting for the shell to render and self-healing\n* on later React re-renders.\n* @param options - the row's attribute/icon/copy/action/ordering configuration.\n* @returns disposer removing the entry and its observers.\n*/\nfunction mountSidebarEntry(options) {\n\tif (typeof document !== \"undefined\" && document.querySelector(options.rowSelector) !== null) return () => {};\n\tconst { entry, applyLabel, applyBadge } = createEntry(options);\n\tlet root;\n\tlet placed = false;\n\tlet unsubscribeRefresh;\n\tif (options.refresh !== void 0) try {\n\t\tunsubscribeRefresh = options.refresh.subscribe(applyLabel);\n\t} catch {}\n\tlet unsubscribeBadge;\n\tif (options.badge !== void 0) try {\n\t\tunsubscribeBadge = options.badge.subscribe(applyBadge);\n\t} catch {}\n\tconst tryPlace = () => {\n\t\tif (root !== void 0 && !root.isConnected) {\n\t\t\trootObserver.disconnect();\n\t\t\troot = void 0;\n\t\t\tplaced = false;\n\t\t}\n\t\tif (placed) {\n\t\t\tif (document.body.contains(entry)) return;\n\t\t\trootObserver.disconnect();\n\t\t\troot = void 0;\n\t\t\tplaced = false;\n\t\t}\n\t\troot ??= sidebarRoot();\n\t\tif (root === void 0) return;\n\t\tplaced = placeEntry(root, entry, options);\n\t\tif (placed) rootObserver.observe(root, {\n\t\t\tchildList: true,\n\t\t\tsubtree: true\n\t\t});\n\t};\n\tconst waitObserver = new MutationObserver(() => {\n\t\ttryPlace();\n\t});\n\twaitObserver.observe(document.body, {\n\t\tchildList: true,\n\t\tsubtree: true\n\t});\n\tconst rootObserver = new MutationObserver(() => {\n\t\tif (root === void 0 || !root.isConnected) {\n\t\t\tplaced = false;\n\t\t\ttryPlace();\n\t\t\treturn;\n\t\t}\n\t\tif (!root.contains(entry)) placed = placeEntry(root, entry, options);\n\t});\n\toptions.active === void 0 || (() => {\n\t\tconst syncActive = () => {\n\t\t\tif (options.active.isOpen()) entry.dataset.active = \"true\";\n\t\t\telse delete entry.dataset.active;\n\t\t};\n\t\tconst unsubscribe = options.active.subscribe(syncActive);\n\t\tsyncActive();\n\t\treturn unsubscribe;\n\t})();\n\ttryPlace();\n\treturn () => {\n\t\twaitObserver.disconnect();\n\t\trootObserver.disconnect();\n\t\tunsubscribeRefresh?.();\n\t\tunsubscribeBadge?.();\n\t\tentry.remove();\n\t};\n}\n/**\n* Every row selector in the Huaqiu sidebar family, in intended visual order.\n*\n* Family ordering is only stable when every participating plugin passes the\n* SAME list to {@link mountSidebarEntry} — the core positions a row relative to\n* the family block, never relative to transient shell geometry. Adding a new\n* family row means adding its selector here (and to no plugin-local list).\n*/\nconst HUAQIU_SIDEBAR_ENTRY_FAMILY = [\n\t\"[data-hqcg-footprint-entry]\",\n\t\"[data-hqcg-symbol-entry]\",\n\t\"[data-hqcg-schematic-entry]\",\n\t\"[data-hq-plugin-update-entry]\"\n];\n/**\n* Read the host UI language from `<html lang>`.\n*\n* `dsh-client-locale` rewrites the attribute on every language change (dsh\n* web) and HQ Edge mirrors it, so this is the one source every family plugin\n* can read without importing a `locale` service.\n* @returns 'zh' / 'en', or undefined when the tag is absent or unsupported\n* (callers fall back to their own default).\n*/\nfunction detectHostLocale() {\n\tif (typeof document === \"undefined\") return void 0;\n\tconst tag = document.documentElement?.getAttribute(\"lang\");\n\tif (tag === null || tag === void 0 || tag === \"\") return void 0;\n\tconst primary = tag.toLowerCase().split(\"-\")[0];\n\treturn primary === \"zh\" || primary === \"en\" ? primary : void 0;\n}\n/**\n* Watch `<html lang>` and report every resolved change.\n*\n* Idempotent-safe: repeated calls share one observer and each caller gets its\n* own disposer; the observer disconnects when the last listener leaves.\n* @param listener - called with the newly resolved language.\n* @returns disposer removing this listener.\n*/\n/** Shared observer/listener set backing {@link observeHostLocale}. */\nconst localeListeners = /* @__PURE__ */ new Set();\nlet localeObserver = null;\nfunction observeHostLocale(listener) {\n\tif (typeof document === \"undefined\" || typeof MutationObserver === \"undefined\" || document.documentElement === null) return () => {};\n\tlet current = detectHostLocale();\n\tconst listeners = localeListeners;\n\tlisteners.add(listener);\n\tif (localeObserver === null) {\n\t\tlocaleObserver = new MutationObserver(() => {\n\t\t\tconst next = detectHostLocale();\n\t\t\tif (next === current) return;\n\t\t\tcurrent = next;\n\t\t\tfor (const fn of [...listeners]) try {\n\t\t\t\tfn(next);\n\t\t\t} catch {}\n\t\t});\n\t\tlocaleObserver.observe(document.documentElement, {\n\t\t\tattributes: true,\n\t\t\tattributeFilter: [\"lang\"]\n\t\t});\n\t}\n\treturn () => {\n\t\tlisteners.delete(listener);\n\t\tif (listeners.size === 0 && localeObserver !== null) {\n\t\t\tlocaleObserver.disconnect();\n\t\t\tlocaleObserver = null;\n\t\t}\n\t};\n}\n//#endregion\nexport { HUAQIU_SIDEBAR_ENTRY_FAMILY, detectHostLocale, mountSidebarEntry, observeHostLocale };\n","/**\n * Plugin-update API client (browser half).\n *\n * Every call is a same-origin fetch against the DSH webServer prefix route\n * (`/api/v1/huaqiu/plugin-update/…`) which the node half forwards to hq-edge.\n * Response shapes mirror hq-edge `apps/server/src/routes/plugin-updates.ts`.\n *\n * @module\n */\n\nexport const UPDATE_BASE = '/api/v1/huaqiu/plugin-update'\n\nexport interface PluginUpdateItem {\n name: string\n currentVersion: string\n availableVersion: string\n state: 'current' | 'update' | 'install' | 'blocked' | 'ignored'\n blockedBy?: string[]\n ignored?: boolean\n reasons?: string[]\n notes?: string[]\n}\n\nexport interface RuntimeUpdateInfo {\n configured: boolean\n currentVersion?: string\n latest?: string\n updateAvailable: boolean\n checkedAt?: string\n lastError?: string\n}\n\nexport interface PluginUpdateReport {\n manifestVersion: string\n pluginSetVersion?: string\n items: PluginUpdateItem[]\n actionableCount: number\n ignoredSet: boolean\n fetchedAt?: string\n lastError?: string\n runtime: RuntimeUpdateInfo\n}\n\nasync function request<T>(path: string, init?: RequestInit): Promise<T> {\n const res = await fetch(`${UPDATE_BASE}${path}`, {\n cache: 'no-store',\n ...init,\n headers: { 'content-type': 'application/json; charset=utf-8', ...(init?.headers ?? {}) },\n })\n const body = (await res.json().catch(() => null)) as T | null\n if (!res.ok) {\n const err = (body as { error?: string } | null)?.error ?? `HTTP ${res.status}`\n throw new Error(err)\n }\n if (body === null) throw new Error('empty response')\n return body\n}\n\n/** GET the cached (or empty-shell) plugin-update report. */\nexport function fetchReport(): Promise<PluginUpdateReport> {\n return request<PluginUpdateReport>('/report')\n}\n\n/** POST an explicit refresh; returns the freshly computed report. */\nexport function checkUpdates(): Promise<PluginUpdateReport> {\n return request<PluginUpdateReport>('/check', { method: 'POST' })\n}\n\nexport interface InstallResult {\n ok: boolean\n installedSetVersion?: string\n installed?: number\n skipped?: number\n restarting?: boolean\n error?: string\n}\n\n/** POST the install transaction for the confirmed set version. */\nexport function installSet(setVersion: string): Promise<InstallResult> {\n return request<InstallResult>('/install', {\n method: 'POST',\n body: JSON.stringify({ setVersion }),\n })\n}\n\n/** POST ignore for the current offer (set-scoped, persists across restarts). */\nexport function ignoreOffer(setVersion: string): Promise<unknown> {\n return request<unknown>('/ignore', { method: 'POST', body: JSON.stringify({ setVersion }) })\n}\n\n/** POST unignore for the set version (reverses an ignore). */\nexport function unignoreOffer(setVersion: string): Promise<unknown> {\n return request<unknown>('/unignore', { method: 'POST', body: JSON.stringify({ setVersion }) })\n}\n","/**\n * Localized copy for the plugin-update UI (browser half).\n *\n * zh is the fallback, matching the rest of the Huaqiu plugin family\n * (`<html lang>` drives the resolution via the shared sidebar-entry locale\n * helpers).\n *\n * @module\n */\n\nconst zh = {\n entryLabel: '插件更新',\n entryTooltip: '查看可用的插件更新',\n viewTitle: '插件更新',\n refresh: '刷新',\n close: '关闭',\n empty: '没有可用的插件更新',\n checkFailed: '检查更新失败',\n manifestVersion: '插件集',\n ignoredOffer: '已忽略当前插件集更新',\n unignore: '恢复提醒',\n installAll: (count: number) => `安装 ${count} 项更新`,\n installing: '安装中…(DSH 将自动重启)',\n state: {\n current: '当前',\n update: '可更新',\n install: '待安装',\n ignored: '已忽略',\n blocked: '被阻止',\n },\n blockedReason: {\n host: '不适用于当前 EDA 主机',\n 'hq-edge-version': '当前 HQ Edge 版本不兼容',\n 'runtime-unknown': 'HQ Edge 版本未知,无法校验',\n },\n runtime: {\n title: 'HQ Edge 运行时',\n updateAvailable: (version: string) => `发现新版本 ${version}`,\n current: (version: string) => `当前版本 ${version}`,\n unknown: '版本未知',\n notConfigured: '未配置更新源',\n note: '运行时更新由 EDA 安装器处理',\n },\n lastError: '上次检查失败',\n fetchedAt: '检查时间',\n installError: '安装失败',\n installSuccess: '更新已安装,正在重启 DSH…',\n}\n\nexport type UpdateCopy = typeof zh\n\nconst en: UpdateCopy = {\n entryLabel: 'Plugin Updates',\n entryTooltip: 'Review available plugin updates',\n viewTitle: 'Plugin Updates',\n refresh: 'Refresh',\n close: 'Close',\n empty: 'No plugin updates available',\n checkFailed: 'Failed to check for updates',\n manifestVersion: 'Plugin set',\n ignoredOffer: 'The current plugin-set update is ignored',\n unignore: 'Show again',\n installAll: (count) => `Install ${count} update${count === 1 ? '' : 's'}`,\n installing: 'Installing… (DSH will restart)',\n state: {\n current: 'Current',\n update: 'Update',\n install: 'Install',\n ignored: 'Ignored',\n blocked: 'Blocked',\n },\n blockedReason: {\n host: 'Not applicable to the current EDA host',\n 'hq-edge-version': 'Incompatible with the current HQ Edge version',\n 'runtime-unknown': 'HQ Edge version unknown — cannot verify',\n },\n runtime: {\n title: 'HQ Edge Runtime',\n updateAvailable: (version) => `New version ${version} available`,\n current: (version) => `Current version ${version}`,\n unknown: 'Version unknown',\n notConfigured: 'No update source configured',\n note: 'Runtime updates are handled by the EDA installer',\n },\n lastError: 'Last check failed',\n fetchedAt: 'Checked at',\n installError: 'Install failed',\n installSuccess: 'Update installed — restarting DSH…',\n}\n\nexport const COPY = { zh, en } as const\n\n","/**\n * Plugin-update review modal (browser half).\n *\n * Plain-DOM modal (no React tree) so it can never disturb the DSH shell's\n * reconciliation — same rule as the sidebar entry rows. Rendered content:\n *\n * - header: title + refresh\n * - plugin list (name, current → available, state badge, blocked reason)\n * - ignored-offer banner + reversible \"show again\"\n * - install action (disabled while installing / nothing actionable / offer\n * ignored); after install the UI keeps polling the backend state instead\n * of trusting the HTTP response alone, because activating a set restarts\n * DSH (spec §7.1: never report success on the 200/202 alone).\n * - independent HQ Edge runtime section (read-only report; the actual\n * upgrade is owned by the EDA installer).\n *\n * @module\n */\nimport {\n checkUpdates,\n ignoreOffer,\n installSet,\n unignoreOffer,\n type PluginUpdateItem,\n type PluginUpdateReport,\n} from './api.js'\nimport type { UpdateCopy } from './copy.js'\n\nexport interface UpdateStoreLike {\n report(): PluginUpdateReport | null\n installing(): boolean\n refresh(): Promise<PluginUpdateReport | null>\n subscribe(listener: () => void): () => void\n}\n\nfunction el<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n className: string,\n text?: string,\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag)\n node.className = className\n if (text !== undefined) node.textContent = text\n return node\n}\n\nfunction stateLabel(item: PluginUpdateItem, copy: UpdateCopy): string {\n switch (item.state) {\n case 'current':\n return copy.state.current\n case 'update':\n return copy.state.update\n case 'install':\n return copy.state.install\n case 'ignored':\n return copy.state.ignored\n case 'blocked':\n return copy.state.blocked\n }\n}\n\nfunction blockedReason(item: PluginUpdateItem, copy: UpdateCopy): string | undefined {\n const by = item.blockedBy ?? []\n if (by.includes('host')) return copy.blockedReason.host\n if (by.includes('hq-edge-version')) return copy.blockedReason['hq-edge-version']\n if (by.includes('runtime-unknown')) return copy.blockedReason['runtime-unknown']\n return by.length > 0 ? by.join(', ') : undefined\n}\n\n/**\n * Open (or focus) the update modal. Re-renders from the store; when the store\n * is stale the modal first refreshes the report.\n */\nexport function openUpdateView(store: UpdateStoreLike, copy: UpdateCopy): void {\n const existing = document.querySelector<HTMLElement>('[data-hq-plugin-update-view]')\n if (existing !== null) {\n existing.style.display = ''\n return\n }\n\n const overlay = el('div', 'hqpu-overlay')\n overlay.setAttribute('data-hq-plugin-update-view', '')\n const panel = el('div', 'hqpu-panel')\n\n // Header\n const header = el('div', 'hqpu-header')\n header.append(el('div', 'hqpu-title', copy.viewTitle))\n const refreshBtn = el('button', 'hqpu-btn', copy.refresh)\n header.append(refreshBtn)\n const closeBtn = el('button', 'hqpu-btn hqpu-btn-ghost', copy.close)\n header.append(closeBtn)\n\n // Body: plugin list + ignored banner + install action + runtime section.\n const body = el('div', 'hqpu-body')\n const listSection = el('div', 'hqpu-section')\n const runtimeSection = el('div', 'hqpu-section hqpu-runtime')\n const actionBar = el('div', 'hqpu-actions')\n\n panel.append(header, body)\n body.append(listSection, runtimeSection, actionBar)\n overlay.append(panel)\n document.body.appendChild(overlay)\n\n const disposers: Array<() => void> = []\n\n const render = (): void => {\n const report = store.report()\n const installing = store.installing()\n renderList(listSection, report, copy, installing, store)\n renderRuntime(runtimeSection, report, copy)\n renderActions(actionBar, report, copy, store, installing)\n }\n\n const unsubscribe = store.subscribe(render)\n disposers.push(unsubscribe)\n\n // Refresh on open (bounded; never block the shell).\n void store.refresh().then(() => {\n // Store notification re-renders already; nothing extra to do.\n })\n\n const onClose = (): void => {\n for (const dispose of disposers) {\n try {\n dispose()\n } catch {\n /* already disposed */\n }\n }\n overlay.remove()\n }\n closeBtn.addEventListener('click', onClose)\n overlay.addEventListener('click', (ev) => {\n if (ev.target === overlay) onClose()\n })\n refreshBtn.addEventListener('click', () => {\n void store.refresh()\n })\n disposers.push(() => {\n closeBtn.removeEventListener('click', onClose)\n overlay.removeEventListener('click', onClose)\n refreshBtn.removeEventListener('click', () => {})\n })\n\n render()\n}\n\nfunction renderList(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n installing: boolean,\n store: UpdateStoreLike,\n): void {\n section.textContent = ''\n if (report === null) {\n section.append(el('div', 'hqpu-empty', copy.checkFailed))\n return\n }\n if (report.items.length === 0) {\n section.append(el('div', 'hqpu-empty', copy.empty))\n return\n }\n\n const manifestRow = el('div', 'hqpu-manifest')\n manifestRow.append(\n el('span', 'hqpu-manifest-label', `${copy.manifestVersion}`),\n el('span', 'hqpu-manifest-value', report.manifestVersion),\n )\n section.append(manifestRow)\n\n if (report.ignoredSet) {\n const banner = el('div', 'hqpu-ignored-banner', copy.ignoredOffer)\n const unignore = el('button', 'hqpu-btn hqpu-btn-small', copy.unignore)\n unignore.addEventListener('click', () => {\n void unignoreOffer(report.manifestVersion)\n .then(() => store.refresh())\n .catch(() => undefined)\n })\n banner.append(unignore)\n section.append(banner)\n }\n\n const list = el('ul', 'hqpu-list')\n for (const item of report.items) {\n const row = el('li', `hqpu-item hqpu-item-${item.state}`)\n const name = el('span', 'hqpu-item-name', item.name)\n const version = el(\n 'span',\n 'hqpu-item-version',\n item.state === 'current'\n ? item.currentVersion\n : `${item.currentVersion} → ${item.availableVersion}`,\n )\n const state = el('span', `hqpu-item-state hqpu-state-${item.state}`, stateLabel(item, copy))\n row.append(name, version, state)\n const reason = blockedReason(item, copy)\n if (reason !== undefined) {\n row.append(el('span', 'hqpu-item-reason', reason))\n }\n list.append(row)\n }\n section.append(list)\n}\n\nfunction renderRuntime(\n section: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n): void {\n section.textContent = ''\n const title = el('div', 'hqpu-runtime-title', copy.runtime.title)\n section.append(title)\n const runtime = report?.runtime\n if (runtime === undefined || runtime.configured === false) {\n section.append(el('div', 'hqpu-runtime-line', copy.runtime.notConfigured))\n return\n }\n if (runtime.updateAvailable && runtime.latest !== undefined) {\n section.append(el('div', 'hqpu-runtime-line', copy.runtime.updateAvailable(runtime.latest)))\n } else if (runtime.currentVersion !== undefined) {\n section.append(el('div', 'hqpu-runtime-line', copy.runtime.current(runtime.currentVersion)))\n } else {\n section.append(el('div', 'hqpu-runtime-line', copy.runtime.unknown))\n }\n section.append(el('div', 'hqpu-runtime-note', copy.runtime.note))\n if (runtime.lastError !== undefined && runtime.lastError.length > 0) {\n section.append(el('div', 'hqpu-runtime-error', `${copy.lastError}: ${runtime.lastError}`))\n }\n}\n\nfunction renderActions(\n bar: HTMLElement,\n report: PluginUpdateReport | null,\n copy: UpdateCopy,\n store: UpdateStoreLike,\n installing: boolean,\n): void {\n bar.textContent = ''\n if (report === null) return\n\n if (installing) {\n bar.append(el('span', 'hqpu-installing', copy.installing))\n return\n }\n\n const actionable = report.items.filter(\n (item) => item.state === 'update' || item.state === 'install',\n ).length\n const installBtn = el(\n 'button',\n 'hqpu-btn hqpu-btn-primary',\n actionable > 0 ? copy.installAll(actionable) : copy.state.current,\n )\n installBtn.disabled = actionable === 0 || report.ignoredSet\n installBtn.addEventListener('click', () => {\n void installSet(report.manifestVersion)\n .then((result) => {\n if (result.error !== undefined) {\n bar.textContent = ''\n bar.append(el('div', 'hqpu-error', `${copy.installError}: ${result.error}`))\n return\n }\n // The set is activated atomically; DSH restarts. Keep polling the\n // backend state rather than trusting the response alone (spec §7.1).\n bar.textContent = ''\n bar.append(el('span', 'hqpu-installing', copy.installSuccess))\n void pollUntilSettled(store, report.manifestVersion)\n })\n .catch((err: unknown) => {\n bar.textContent = ''\n bar.append(\n el('div', 'hqpu-error', `${copy.installError}: ${err instanceof Error ? err.message : String(err)}`),\n )\n })\n })\n bar.append(installBtn)\n}\n\n/** After an install, poll the backend until the update report settles\n * (installing flag clears or DSH restarts under us — the next load re-reads\n * the fresh state). */\nfunction pollUntilSettled(store: UpdateStoreLike, setVersion: string): void {\n let attempts = 0\n const tick = (): void => {\n void store\n .refresh()\n .then((report) => {\n if (store.installing()) return\n if (report !== null && report.ignoredSet === false && attempts >= 2) return\n })\n .catch(() => undefined)\n .finally(() => {\n attempts += 1\n if (attempts < 10) {\n window.setTimeout(tick, 2000)\n }\n })\n }\n tick()\n}\n","/**\n * `@huaqiu/dsh-tool-plugin-update` — browser half.\n *\n * Mounts one sidebar toolbutton in the Huaqiu family block (\"插件更新\") with a\n * numeric badge equal to the actionable plugin-update count (spec §6.3/§7.1:\n * only compatible `update` + `install` items after host/HQ-Edge filtering and\n * after excluding the ignored offer; runtime updates are NOT counted). Click\n * opens the review/confirm/ignore modal.\n *\n * The authoritative state is the hq-edge report fetched through the plugin's\n * same-origin forwarding routes. A tiny in-memory store owns the latest report\n * + installing flag; the sidebar badge and the modal subscribe to it. No\n * timers beyond the install-poll and an idle 60 s refresh.\n *\n * @module\n */\nimport {\n HUAQIU_SIDEBAR_ENTRY_FAMILY,\n detectHostLocale,\n mountSidebarEntry,\n observeHostLocale,\n type HostLocale,\n} from '@huaqiu/dsh-sidebar-entry'\nimport { checkUpdates, fetchReport, type PluginUpdateReport } from './api.js'\nimport { COPY } from './copy.js'\nimport { openUpdateView, type UpdateStoreLike } from './update-view.js'\n\n/** Stable data attributes identifying the injected entry row. */\nexport const UPDATE_ENTRY_SELECTOR = '[data-hq-plugin-update-entry]'\nexport const UPDATE_ENTRY_ATTRIBUTE = 'data-hq-plugin-update-entry'\n\n/** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */\nconst PLUGIN_ID = 'huaqiu-plugin-update'\n\n/** Row class names (defined by {@link ENTRY_CSS_TEXT}). */\nconst ENTRY_CSS: Record<string, string> = {\n entry: 'hqpu-sidebar-entry',\n entryIcon: 'hqpu-sidebar-entry__icon',\n entryLabel: 'hqpu-sidebar-entry__label',\n entryBadge: 'hqpu-sidebar-entry__badge',\n}\n\n/** Localized row copy: zh is the fallback, matching the family convention. */\nfunction entryCopy(lang: HostLocale | undefined): { label: string; tooltip: string } {\n return lang === 'en'\n ? { label: COPY.en.entryLabel, tooltip: COPY.en.entryTooltip }\n : { label: COPY.zh.entryLabel, tooltip: COPY.zh.entryTooltip }\n}\n\n/** Inline update glyph — `currentColor`, 16×16 like the shell's nav glyphs. */\nexport const UPDATE_ICON =\n`<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\">\n <g fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path d=\"M2.5 8a5.5 5.5 0 1 1 1.61 3.89\" />\n <path d=\"M2.5 8V4.75M2.5 8h3.25\" />\n </g>\n</svg>`\n\n/** Sidebar entry styles — matched to the family rows (same inset/padding/gap\n * as the shell's own Plugins entry). The badge is a 16px rounded pill. */\nconst ENTRY_CSS_TEXT = `\n.hqpu-sidebar-entry {\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin: 0 2px;\n min-height: 36px;\n padding: 7px 8px;\n background: transparent;\n border: none;\n border-radius: 12px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font: inherit;\n line-height: 22px;\n text-align: left;\n white-space: nowrap;\n}\n.hqpu-sidebar-entry:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.hqpu-sidebar-entry[data-active] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.hqpu-sidebar-entry__icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n}\n.hqpu-sidebar-entry__icon svg {\n display: block;\n width: 16px;\n height: 16px;\n}\n.hqpu-sidebar-entry__label {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1;\n}\n.hqpu-sidebar-entry__badge {\n flex: none;\n min-width: 16px;\n height: 16px;\n padding: 0 5px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-badge, #e5484d);\n color: #fff;\n font-size: 11px;\n line-height: 16px;\n text-align: center;\n}\n/* Collapsed rail: icon-only, centered — same 36px square the shell uses. */\n[data-sidebar-collapsed] .hqpu-sidebar-entry {\n justify-content: center;\n width: 36px;\n height: 36px;\n margin: 0;\n padding: 0;\n}\n[data-sidebar-collapsed] .hqpu-sidebar-entry__icon svg {\n width: 18px;\n height: 18px;\n}\n[data-sidebar-collapsed] .hqpu-sidebar-entry__label,\n[data-sidebar-collapsed] .hqpu-sidebar-entry__badge {\n display: none;\n}\n`\n\n/** Modal styles (hqpu-*). Plain overlay + panel, aligned to the shell's\n * dark/light aliases. */\nconst VIEW_CSS_TEXT = `\n.hqpu-overlay {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.45);\n}\n.hqpu-panel {\n box-sizing: border-box;\n width: min(640px, calc(100vw - 48px));\n max-height: min(80vh, 720px);\n display: flex;\n flex-direction: column;\n border-radius: 16px;\n background: var(--dsw-alias-bg-surface, #fff);\n color: var(--dsw-alias-label-primary, #1f2329);\n box-shadow: 0 24px 64px rgba(0, 0, 0, 0.24);\n overflow: hidden;\n}\n.hqpu-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 14px 16px;\n border-bottom: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));\n}\n.hqpu-title {\n flex: 1;\n font-size: 15px;\n font-weight: 600;\n}\n.hqpu-body {\n overflow: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n.hqpu-section {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.hqpu-manifest {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, #646a73);\n}\n.hqpu-manifest-value {\n font-weight: 600;\n color: var(--dsw-alias-label-primary, #1f2329);\n}\n.hqpu-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n.hqpu-item {\n display: grid;\n grid-template-columns: 1fr auto auto;\n gap: 8px;\n align-items: center;\n padding: 8px 10px;\n border-radius: 10px;\n background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.04));\n font-size: 13px;\n}\n.hqpu-item-name {\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.hqpu-item-version {\n color: var(--dsw-alias-label-secondary, #646a73);\n font-variant-numeric: tabular-nums;\n}\n.hqpu-item-state {\n padding: 1px 8px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 18px;\n}\n.hqpu-state-current { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }\n.hqpu-state-update, .hqpu-state-install { background: rgba(229,72,77,.12); color: #d92d33; }\n.hqpu-state-ignored { background: rgba(0,0,0,.06); color: var(--dsw-alias-label-secondary, #646a73); }\n.hqpu-state-blocked { background: rgba(229,72,77,.12); color: #d92d33; }\n.hqpu-item-reason {\n grid-column: 1 / -1;\n font-size: 12px;\n color: var(--dsw-alias-label-tertiary, #8f959e);\n}\n.hqpu-ignored-banner {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 10px;\n border-radius: 10px;\n background: rgba(120,120,120,.1);\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, #646a73);\n}\n.hqpu-empty {\n padding: 24px 0;\n text-align: center;\n color: var(--dsw-alias-label-tertiary, #8f959e);\n font-size: 13px;\n}\n.hqpu-runtime {\n border-top: 1px solid var(--dsw-alias-line-border, rgba(0,0,0,.08));\n padding-top: 12px;\n}\n.hqpu-runtime-title {\n font-size: 13px;\n font-weight: 600;\n}\n.hqpu-runtime-line {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, #646a73);\n}\n.hqpu-runtime-note {\n font-size: 11px;\n color: var(--dsw-alias-label-tertiary, #8f959e);\n}\n.hqpu-runtime-error {\n font-size: 12px;\n color: #d92d33;\n}\n.hqpu-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.hqpu-btn {\n box-sizing: border-box;\n padding: 6px 14px;\n border-radius: 8px;\n border: 1px solid transparent;\n background: var(--dsw-alias-interactive-bg-hover, rgba(0,0,0,.06));\n color: var(--dsw-alias-label-primary, #1f2329);\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n.hqpu-btn-primary {\n background: #e5484d;\n color: #fff;\n}\n.hqpu-btn-primary:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.hqpu-btn-ghost {\n background: transparent;\n border-color: var(--dsw-alias-line-border, rgba(0,0,0,.12));\n}\n.hqpu-btn-small {\n padding: 2px 10px;\n font-size: 12px;\n}\n.hqpu-installing {\n font-size: 13px;\n color: var(--dsw-alias-label-secondary, #646a73);\n}\n.hqpu-error {\n font-size: 13px;\n color: #d92d33;\n}\n`\n\nconst STYLE_TAG_ID = 'data-plugin-css=\"huaqiu-plugin-update\"'\n\n/** Inject styles once (idempotent). */\nexport function injectUpdateStyles(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return\n const tag = document.createElement('style')\n tag.dataset.pluginCss = 'huaqiu-plugin-update'\n tag.textContent = `${ENTRY_CSS_TEXT}\\n${VIEW_CSS_TEXT}`\n document.head.appendChild(tag)\n}\n\n/** Minimal structural client context (dsh-client-runtime provides these). */\nexport interface ClientContext {\n get<T = unknown>(name: string): T | undefined\n effect?(fn: () => (() => void) | void, label?: string): void\n}\n\n/** In-memory update-report store shared by the badge and the modal. */\nclass UpdateStore implements UpdateStoreLike {\n private current: PluginUpdateReport | null = null\n private installingFlag = false\n private readonly listeners = new Set<() => void>()\n private refreshTimer: number | null = null\n\n report(): PluginUpdateReport | null {\n return this.current\n }\n\n installing(): boolean {\n return this.installingFlag\n }\n\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /** Badge count per spec §6.3: actionable `update` + `install`, excluding the\n * ignored offer (the server already folds ignore into item states; keep the\n * client-side filter as a defensive second pass). */\n badgeCount(): number {\n const report = this.current\n if (report === null) return 0\n return report.items.filter(\n (item) => item.state === 'update' || item.state === 'install',\n ).length\n }\n\n private emit(): void {\n for (const listener of [...this.listeners]) {\n try {\n listener()\n } catch {\n /* one bad listener must not strand the rest */\n }\n }\n }\n\n async refresh(): Promise<PluginUpdateReport | null> {\n try {\n const report = await fetchReport()\n this.current = report\n this.emit()\n return report\n } catch {\n return this.current\n }\n }\n\n async check(): Promise<PluginUpdateReport | null> {\n try {\n const report = await checkUpdates()\n this.current = report\n this.emit()\n return report\n } catch {\n return this.current\n }\n }\n\n /** Mark the set as installing (set by the modal before the restart). */\n setInstalling(flag: boolean): void {\n this.installingFlag = flag\n this.emit()\n }\n\n /** Idle refresh while mounted (60 s) — no overlapping poll loop. */\n startIdleRefresh(): void {\n if (this.refreshTimer !== null) return\n this.refreshTimer = window.setInterval(() => {\n void this.refresh()\n }, 60_000) as unknown as number\n }\n\n stop(): void {\n if (this.refreshTimer !== null) {\n window.clearInterval(this.refreshTimer)\n this.refreshTimer = null\n }\n this.listeners.clear()\n }\n}\n\n/**\n * Client cordis inject: no service dependencies — the plugin reads the\n * hq-edge report purely through same-origin fetch (forwarded by its own node\n * half). `inject` stays empty so the loader never waits on another service.\n */\nexport const inject: string[] = []\n\n/**\n * Client plugin body — mount the sidebar toolbutton + review modal.\n * @param ctx - real cordis client context.\n * @returns disposer removing the row and all subscriptions.\n */\nexport function apply(ctx: ClientContext): () => void {\n if (typeof document === 'undefined') return () => {}\n injectUpdateStyles()\n\n const store = new UpdateStore()\n const copy = { zh: COPY.zh, en: COPY.en }\n const copyFor = (): typeof COPY.zh => (locale === 'en' ? copy.en : copy.zh)\n\n // Locale: re-render the row copy when <html lang> changes. The sidebar\n // helper subscribes via `refresh.subscribe`; we fan out to its listener.\n let locale: HostLocale | undefined = detectHostLocale()\n const localeSubscribers = new Set<() => void>()\n const refreshSubscribe = (listener: () => void): (() => void) => {\n localeSubscribers.add(listener)\n return () => {\n localeSubscribers.delete(listener)\n }\n }\n const unsubscribeLocale = observeHostLocale((next) => {\n locale = next\n for (const fn of [...localeSubscribers]) {\n try {\n fn()\n } catch {\n /* one bad subscriber must not strand the rest */\n }\n }\n })\n const labelState = { label: entryCopy(locale).label, tooltip: entryCopy(locale).tooltip }\n const applyLabel = (): void => {\n const c = entryCopy(locale)\n labelState.label = c.label\n labelState.tooltip = c.tooltip\n }\n\n const openView = (): void => {\n // Always re-check on open so the review screen never shows stale data.\n void store.check().then((report) => {\n if (report !== null) openUpdateView(store, copyFor())\n })\n }\n\n const disposeEntry = mountSidebarEntry({\n rowAttribute: UPDATE_ENTRY_ATTRIBUTE,\n rowSelector: UPDATE_ENTRY_SELECTOR,\n plugin: PLUGIN_ID,\n icon: UPDATE_ICON,\n css: ENTRY_CSS,\n label: () => labelState.label,\n tooltip: () => labelState.tooltip,\n refresh: { subscribe: refreshSubscribe },\n badge: {\n subscribe: (listener) => store.subscribe(listener),\n count: () => store.badgeCount(),\n },\n onToggle: openView,\n position: 'after',\n familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY,\n })\n\n store.startIdleRefresh()\n void store.refresh()\n\n const dispose = (): void => {\n try {\n disposeEntry()\n } catch {\n /* already disposed */\n }\n unsubscribeLocale()\n store.stop()\n }\n if (typeof ctx.effect === 'function') {\n ctx.effect(dispose, 'plugin-update')\n }\n return dispose\n}\n"],"mappings":";;;;;;;;EAEA,SAAS,cAAc;GACtB,MAAM,SAAS,SAAS,cAAc,kDAAkD;GACxF,IAAI,WAAW,MAAM,OAAO,KAAK;GACjC,OAAO,OAAO,cAAc,sBAAsB,CAAC,EAAE,iBAAiB,OAAO;EAC9E;;EAEA,SAAS,iBAAiB,MAAM;GAC/B,MAAM,SAAS,KAAK,cAAc,+BAA+B;GACjE,IAAI,WAAW,MAAM,OAAO;GAC5B,KAAK,MAAM,SAAS,KAAK,UAAU,IAAI,MAAM,YAAY,UAAU,OAAO;EAC3E;;EAEA,SAAS,YAAY,SAAS;GAC7B,MAAM,QAAQ,SAAS,cAAc,QAAQ;GAC7C,MAAM,OAAO;GACb,MAAM,aAAa,QAAQ,cAAc,EAAE;GAC3C,IAAI,QAAQ,WAAW,KAAK,GAAG;IAC9B,MAAM,aAAa,mBAAmB,QAAQ,MAAM;IACpD,MAAM,aAAa,iBAAiB,eAAe;GACpD;GACA,MAAM,YAAY,QAAQ,IAAI,YAAY;GAC1C,MAAM,YAAY,SAAS,cAAc,MAAM;GAC/C,UAAU,YAAY,QAAQ,IAAI,iBAAiB;GACnD,MAAM,WAAW,SAAS,cAAc,MAAM;GAC9C,SAAS,YAAY,QAAQ,IAAI,gBAAgB;GACjD,SAAS,YAAY,QAAQ;GAC7B,MAAM,OAAO,UAAU,SAAS;GAChC,IAAI,YAAY;GAChB,MAAM,mBAAmB;IACxB,IAAI,QAAQ,UAAU,KAAK,GAAG;IAC9B,MAAM,QAAQ,QAAQ,MAAM,MAAM;IAClC,IAAI,SAAS,GAAG;KACf,WAAW,OAAO;KAClB,YAAY;KACZ;IACD;IACA,IAAI,cAAc,MAAM;KACvB,YAAY,SAAS,cAAc,MAAM;KACzC,UAAU,YAAY,QAAQ,IAAI,iBAAiB;KACnD,MAAM,OAAO,SAAS;IACvB;IACA,UAAU,cAAc,OAAO,KAAK;IACpC,MAAM,aAAa,kBAAkB,EAAE;GACxC;GACA,MAAM,mBAAmB;IACxB,MAAM,aAAa,cAAc,QAAQ,MAAM,CAAC;IAChD,IAAI,QAAQ,YAAY,KAAK,GAAG,MAAM,aAAa,SAAS,QAAQ,QAAQ,CAAC;IAC7E,UAAU,cAAc,QAAQ,MAAM;GACvC;GACA,WAAW;GACX,WAAW;GACX,MAAM,iBAAiB,SAAS,QAAQ,QAAQ;GAChD,OAAO;IACN;IACA;IACA;GACD;EACD;;EAEA,SAAS,WAAW,MAAM,OAAO,SAAS;GACzC,MAAM,SAAS,iBAAiB,IAAI;GACpC,IAAI,WAAW,KAAK,GAAG,OAAO;GAC9B,IAAI,MAAM,kBAAkB,MAAM;IACjC,MAAM,MAAM,OAAO,QAAQ,sBAAsB;IACjD,MAAM,OAAO,QAAQ,QAAQ,IAAI,kBAAkB,OAAO,MAAM;IAChE,MAAM,SAAS,MAAM,KAAK,KAAK,QAAQ,CAAC,CAAC,QAAQ,OAAO,cAAc,eAAe,GAAG,QAAQ,QAAQ,gBAAgB,KAAK,IAAI,CAAC,CAAC;IACnI,MAAM,SAAS,QAAQ,aAAa,WAAW,OAAO,SAAS,IAAI,OAAO,KAAK,KAAK,qBAAqB,OAAO,SAAS,IAAI,OAAO,OAAO,SAAS,EAAE,CAAC,qBAAqB,KAAK;IACjL,KAAK,aAAa,OAAO,UAAU,IAAI;GACxC;GACA,OAAO;EACR;;;;;;;EAOA,SAAS,kBAAkB,SAAS;GACnC,IAAI,OAAO,aAAa,eAAe,SAAS,cAAc,QAAQ,WAAW,MAAM,MAAM,aAAa,CAAC;GAC3G,MAAM,EAAE,OAAO,YAAY,eAAe,YAAY,OAAO;GAC7D,IAAI;GACJ,IAAI,SAAS;GACb,IAAI;GACJ,IAAI,QAAQ,YAAY,KAAK,GAAG,IAAI;IACnC,qBAAqB,QAAQ,QAAQ,UAAU,UAAU;GAC1D,QAAQ,CAAC;GACT,IAAI;GACJ,IAAI,QAAQ,UAAU,KAAK,GAAG,IAAI;IACjC,mBAAmB,QAAQ,MAAM,UAAU,UAAU;GACtD,QAAQ,CAAC;GACT,MAAM,iBAAiB;IACtB,IAAI,SAAS,KAAK,KAAK,CAAC,KAAK,aAAa;KACzC,aAAa,WAAW;KACxB,OAAO,KAAK;KACZ,SAAS;IACV;IACA,IAAI,QAAQ;KACX,IAAI,SAAS,KAAK,SAAS,KAAK,GAAG;KACnC,aAAa,WAAW;KACxB,OAAO,KAAK;KACZ,SAAS;IACV;IACA,SAAS,YAAY;IACrB,IAAI,SAAS,KAAK,GAAG;IACrB,SAAS,WAAW,MAAM,OAAO,OAAO;IACxC,IAAI,QAAQ,aAAa,QAAQ,MAAM;KACtC,WAAW;KACX,SAAS;IACV,CAAC;GACF;GACA,MAAM,eAAe,IAAI,uBAAuB;IAC/C,SAAS;GACV,CAAC;GACD,aAAa,QAAQ,SAAS,MAAM;IACnC,WAAW;IACX,SAAS;GACV,CAAC;GACD,MAAM,eAAe,IAAI,uBAAuB;IAC/C,IAAI,SAAS,KAAK,KAAK,CAAC,KAAK,aAAa;KACzC,SAAS;KACT,SAAS;KACT;IACD;IACA,IAAI,CAAC,KAAK,SAAS,KAAK,GAAG,SAAS,WAAW,MAAM,OAAO,OAAO;GACpE,CAAC;GACD,QAAQ,WAAW,KAAK,YAAY;IACnC,MAAM,mBAAmB;KACxB,IAAI,QAAQ,OAAO,OAAO,GAAG,MAAM,QAAQ,SAAS;UAC/C,OAAO,MAAM,QAAQ;IAC3B;IACA,MAAM,cAAc,QAAQ,OAAO,UAAU,UAAU;IACvD,WAAW;IACX,OAAO;GACR,EAAA,CAAG;GACH,SAAS;GACT,aAAa;IACZ,aAAa,WAAW;IACxB,aAAa,WAAW;IACxB,qBAAqB;IACrB,mBAAmB;IACnB,MAAM,OAAO;GACd;EACD;;;;;;;;;EASA,MAAM,8BAA8B;GACnC;GACA;GACA;GACA;EACD;;;;;;;;;;EAUA,SAAS,mBAAmB;GAC3B,IAAI,OAAO,aAAa,aAAa,OAAO,KAAK;GACjD,MAAM,MAAM,SAAS,iBAAiB,aAAa,MAAM;GACzD,IAAI,QAAQ,QAAQ,QAAQ,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK;GAC9D,MAAM,UAAU,IAAI,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;GAC7C,OAAO,YAAY,QAAQ,YAAY,OAAO,UAAU,KAAK;EAC9D;;;;;;;;;;EAUA,MAAM,kCAAkC,IAAI,IAAI;EAChD,IAAI,iBAAiB;EACrB,SAAS,kBAAkB,UAAU;GACpC,IAAI,OAAO,aAAa,eAAe,OAAO,qBAAqB,eAAe,SAAS,oBAAoB,MAAM,aAAa,CAAC;GACnI,IAAI,UAAU,iBAAiB;GAC/B,MAAM,YAAY;GAClB,UAAU,IAAI,QAAQ;GACtB,IAAI,mBAAmB,MAAM;IAC5B,iBAAiB,IAAI,uBAAuB;KAC3C,MAAM,OAAO,iBAAiB;KAC9B,IAAI,SAAS,SAAS;KACtB,UAAU;KACV,KAAK,MAAM,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI;MACpC,GAAG,IAAI;KACR,QAAQ,CAAC;IACV,CAAC;IACD,eAAe,QAAQ,SAAS,iBAAiB;KAChD,YAAY;KACZ,iBAAiB,CAAC,MAAM;IACzB,CAAC;GACF;GACA,aAAa;IACZ,UAAU,OAAO,QAAQ;IACzB,IAAI,UAAU,SAAS,KAAK,mBAAmB,MAAM;KACpD,eAAe,WAAW;KAC1B,iBAAiB;IAClB;GACD;EACD;;;;;;;;;;;;EC1MA,MAAa,cAAc;EAiC3B,eAAe,QAAW,MAAc,MAAgC;GACtE,MAAM,MAAM,MAAM,MAAM,GAAG,cAAc,QAAQ;IAC/C,OAAO;IACP,GAAG;IACH,SAAS;KAAE,gBAAgB;KAAmC,GAAI,MAAM,WAAW,CAAC;IAAG;GACzF,CAAC;GACD,MAAM,OAAQ,MAAM,IAAI,KAAK,CAAC,CAAC,YAAY,IAAI;GAC/C,IAAI,CAAC,IAAI,IAAI;IACX,MAAM,MAAO,MAAoC,SAAS,QAAQ,IAAI;IACtE,MAAM,IAAI,MAAM,GAAG;GACrB;GACA,IAAI,SAAS,MAAM,MAAM,IAAI,MAAM,gBAAgB;GACnD,OAAO;EACT;;EAGA,SAAgB,cAA2C;GACzD,OAAO,QAA4B,SAAS;EAC9C;;EAGA,SAAgB,eAA4C;GAC1D,OAAO,QAA4B,UAAU,EAAE,QAAQ,OAAO,CAAC;EACjE;;EAYA,SAAgB,WAAW,YAA4C;GACrE,OAAO,QAAuB,YAAY;IACxC,QAAQ;IACR,MAAM,KAAK,UAAU,EAAE,WAAW,CAAC;GACrC,CAAC;EACH;;EAQA,SAAgB,cAAc,YAAsC;GAClE,OAAO,QAAiB,aAAa;IAAE,QAAQ;IAAQ,MAAM,KAAK,UAAU,EAAE,WAAW,CAAC;GAAE,CAAC;EAC/F;ECHA,MAAa,OAAO;GAAE;IA/EpB,YAAY;IACZ,cAAc;IACd,WAAW;IACX,SAAS;IACT,OAAO;IACP,OAAO;IACP,aAAa;IACb,iBAAiB;IACjB,cAAc;IACd,UAAU;IACV,aAAa,UAAkB,MAAM,MAAM;IAC3C,YAAY;IACZ,OAAO;KACL,SAAS;KACT,QAAQ;KACR,SAAS;KACT,SAAS;KACT,SAAS;IACX;IACA,eAAe;KACb,MAAM;KACN,mBAAmB;KACnB,mBAAmB;IACrB;IACA,SAAS;KACP,OAAO;KACP,kBAAkB,YAAoB,SAAS;KAC/C,UAAU,YAAoB,QAAQ;KACtC,SAAS;KACT,eAAe;KACf,MAAM;IACR;IACA,WAAW;IACX,WAAW;IACX,cAAc;IACd,gBAAgB;GA4CI;GAAI;IAtCxB,YAAY;IACZ,cAAc;IACd,WAAW;IACX,SAAS;IACT,OAAO;IACP,OAAO;IACP,aAAa;IACb,iBAAiB;IACjB,cAAc;IACd,UAAU;IACV,aAAa,UAAU,WAAW,MAAM,SAAS,UAAU,IAAI,KAAK;IACpE,YAAY;IACZ,OAAO;KACL,SAAS;KACT,QAAQ;KACR,SAAS;KACT,SAAS;KACT,SAAS;IACX;IACA,eAAe;KACb,MAAM;KACN,mBAAmB;KACnB,mBAAmB;IACrB;IACA,SAAS;KACP,OAAO;KACP,kBAAkB,YAAY,eAAe,QAAQ;KACrD,UAAU,YAAY,mBAAmB;KACzC,SAAS;KACT,eAAe;KACf,MAAM;IACR;IACA,WAAW;IACX,WAAW;IACX,cAAc;IACd,gBAAgB;GAGQ;EAAG;;;;;;;;;;;;;;;;;;;;;ECvD7B,SAAS,GACP,KACA,WACA,MAC0B;GAC1B,MAAM,OAAO,SAAS,cAAc,GAAG;GACvC,KAAK,YAAY;GACjB,IAAI,SAAS,KAAA,GAAW,KAAK,cAAc;GAC3C,OAAO;EACT;EAEA,SAAS,WAAW,MAAwB,MAA0B;GACpE,QAAQ,KAAK,OAAb;IACE,KAAK,WACH,OAAO,KAAK,MAAM;IACpB,KAAK,UACH,OAAO,KAAK,MAAM;IACpB,KAAK,WACH,OAAO,KAAK,MAAM;IACpB,KAAK,WACH,OAAO,KAAK,MAAM;IACpB,KAAK,WACH,OAAO,KAAK,MAAM;GACtB;EACF;EAEA,SAAS,cAAc,MAAwB,MAAsC;GACnF,MAAM,KAAK,KAAK,aAAa,CAAC;GAC9B,IAAI,GAAG,SAAS,MAAM,GAAG,OAAO,KAAK,cAAc;GACnD,IAAI,GAAG,SAAS,iBAAiB,GAAG,OAAO,KAAK,cAAc;GAC9D,IAAI,GAAG,SAAS,iBAAiB,GAAG,OAAO,KAAK,cAAc;GAC9D,OAAO,GAAG,SAAS,IAAI,GAAG,KAAK,IAAI,IAAI,KAAA;EACzC;;;;;EAMA,SAAgB,eAAe,OAAwB,MAAwB;GAC7E,MAAM,WAAW,SAAS,cAA2B,8BAA8B;GACnF,IAAI,aAAa,MAAM;IACrB,SAAS,MAAM,UAAU;IACzB;GACF;GAEA,MAAM,UAAU,GAAG,OAAO,cAAc;GACxC,QAAQ,aAAa,8BAA8B,EAAE;GACrD,MAAM,QAAQ,GAAG,OAAO,YAAY;GAGpC,MAAM,SAAS,GAAG,OAAO,aAAa;GACtC,OAAO,OAAO,GAAG,OAAO,cAAc,KAAK,SAAS,CAAC;GACrD,MAAM,aAAa,GAAG,UAAU,YAAY,KAAK,OAAO;GACxD,OAAO,OAAO,UAAU;GACxB,MAAM,WAAW,GAAG,UAAU,2BAA2B,KAAK,KAAK;GACnE,OAAO,OAAO,QAAQ;GAGtB,MAAM,OAAO,GAAG,OAAO,WAAW;GAClC,MAAM,cAAc,GAAG,OAAO,cAAc;GAC5C,MAAM,iBAAiB,GAAG,OAAO,2BAA2B;GAC5D,MAAM,YAAY,GAAG,OAAO,cAAc;GAE1C,MAAM,OAAO,QAAQ,IAAI;GACzB,KAAK,OAAO,aAAa,gBAAgB,SAAS;GAClD,QAAQ,OAAO,KAAK;GACpB,SAAS,KAAK,YAAY,OAAO;GAEjC,MAAM,YAA+B,CAAC;GAEtC,MAAM,eAAqB;IACzB,MAAM,SAAS,MAAM,OAAO;IAC5B,MAAM,aAAa,MAAM,WAAW;IACpC,WAAW,aAAa,QAAQ,MAAM,YAAY,KAAK;IACvD,cAAc,gBAAgB,QAAQ,IAAI;IAC1C,cAAc,WAAW,QAAQ,MAAM,OAAO,UAAU;GAC1D;GAEA,MAAM,cAAc,MAAM,UAAU,MAAM;GAC1C,UAAU,KAAK,WAAW;GAG1B,MAAW,QAAQ,CAAC,CAAC,WAAW,CAEhC,CAAC;GAED,MAAM,gBAAsB;IAC1B,KAAK,MAAM,WAAW,WACpB,IAAI;KACF,QAAQ;IACV,QAAQ,CAER;IAEF,QAAQ,OAAO;GACjB;GACA,SAAS,iBAAiB,SAAS,OAAO;GAC1C,QAAQ,iBAAiB,UAAU,OAAO;IACxC,IAAI,GAAG,WAAW,SAAS,QAAQ;GACrC,CAAC;GACD,WAAW,iBAAiB,eAAe;IACzC,MAAW,QAAQ;GACrB,CAAC;GACD,UAAU,WAAW;IACnB,SAAS,oBAAoB,SAAS,OAAO;IAC7C,QAAQ,oBAAoB,SAAS,OAAO;IAC5C,WAAW,oBAAoB,eAAe,CAAC,CAAC;GAClD,CAAC;GAED,OAAO;EACT;EAEA,SAAS,WACP,SACA,QACA,MACA,YACA,OACM;GACN,QAAQ,cAAc;GACtB,IAAI,WAAW,MAAM;IACnB,QAAQ,OAAO,GAAG,OAAO,cAAc,KAAK,WAAW,CAAC;IACxD;GACF;GACA,IAAI,OAAO,MAAM,WAAW,GAAG;IAC7B,QAAQ,OAAO,GAAG,OAAO,cAAc,KAAK,KAAK,CAAC;IAClD;GACF;GAEA,MAAM,cAAc,GAAG,OAAO,eAAe;GAC7C,YAAY,OACV,GAAG,QAAQ,uBAAuB,GAAG,KAAK,iBAAiB,GAC3D,GAAG,QAAQ,uBAAuB,OAAO,eAAe,CAC1D;GACA,QAAQ,OAAO,WAAW;GAE1B,IAAI,OAAO,YAAY;IACrB,MAAM,SAAS,GAAG,OAAO,uBAAuB,KAAK,YAAY;IACjE,MAAM,WAAW,GAAG,UAAU,2BAA2B,KAAK,QAAQ;IACtE,SAAS,iBAAiB,eAAe;KACvC,cAAmB,OAAO,eAAe,CAAC,CACvC,WAAW,MAAM,QAAQ,CAAC,CAAC,CAC3B,YAAY,KAAA,CAAS;IAC1B,CAAC;IACD,OAAO,OAAO,QAAQ;IACtB,QAAQ,OAAO,MAAM;GACvB;GAEA,MAAM,OAAO,GAAG,MAAM,WAAW;GACjC,KAAK,MAAM,QAAQ,OAAO,OAAO;IAC/B,MAAM,MAAM,GAAG,MAAM,uBAAuB,KAAK,OAAO;IACxD,MAAM,OAAO,GAAG,QAAQ,kBAAkB,KAAK,IAAI;IACnD,MAAM,UAAU,GACd,QACA,qBACA,KAAK,UAAU,YACX,KAAK,iBACL,GAAG,KAAK,eAAe,KAAK,KAAK,kBACvC;IACA,MAAM,QAAQ,GAAG,QAAQ,8BAA8B,KAAK,SAAS,WAAW,MAAM,IAAI,CAAC;IAC3F,IAAI,OAAO,MAAM,SAAS,KAAK;IAC/B,MAAM,SAAS,cAAc,MAAM,IAAI;IACvC,IAAI,WAAW,KAAA,GACb,IAAI,OAAO,GAAG,QAAQ,oBAAoB,MAAM,CAAC;IAEnD,KAAK,OAAO,GAAG;GACjB;GACA,QAAQ,OAAO,IAAI;EACrB;EAEA,SAAS,cACP,SACA,QACA,MACM;GACN,QAAQ,cAAc;GACtB,MAAM,QAAQ,GAAG,OAAO,sBAAsB,KAAK,QAAQ,KAAK;GAChE,QAAQ,OAAO,KAAK;GACpB,MAAM,UAAU,QAAQ;GACxB,IAAI,YAAY,KAAA,KAAa,QAAQ,eAAe,OAAO;IACzD,QAAQ,OAAO,GAAG,OAAO,qBAAqB,KAAK,QAAQ,aAAa,CAAC;IACzE;GACF;GACA,IAAI,QAAQ,mBAAmB,QAAQ,WAAW,KAAA,GAChD,QAAQ,OAAO,GAAG,OAAO,qBAAqB,KAAK,QAAQ,gBAAgB,QAAQ,MAAM,CAAC,CAAC;QACtF,IAAI,QAAQ,mBAAmB,KAAA,GACpC,QAAQ,OAAO,GAAG,OAAO,qBAAqB,KAAK,QAAQ,QAAQ,QAAQ,cAAc,CAAC,CAAC;QAE3F,QAAQ,OAAO,GAAG,OAAO,qBAAqB,KAAK,QAAQ,OAAO,CAAC;GAErE,QAAQ,OAAO,GAAG,OAAO,qBAAqB,KAAK,QAAQ,IAAI,CAAC;GAChE,IAAI,QAAQ,cAAc,KAAA,KAAa,QAAQ,UAAU,SAAS,GAChE,QAAQ,OAAO,GAAG,OAAO,sBAAsB,GAAG,KAAK,UAAU,IAAI,QAAQ,WAAW,CAAC;EAE7F;EAEA,SAAS,cACP,KACA,QACA,MACA,OACA,YACM;GACN,IAAI,cAAc;GAClB,IAAI,WAAW,MAAM;GAErB,IAAI,YAAY;IACd,IAAI,OAAO,GAAG,QAAQ,mBAAmB,KAAK,UAAU,CAAC;IACzD;GACF;GAEA,MAAM,aAAa,OAAO,MAAM,QAC7B,SAAS,KAAK,UAAU,YAAY,KAAK,UAAU,SACtD,CAAC,CAAC;GACF,MAAM,aAAa,GACjB,UACA,6BACA,aAAa,IAAI,KAAK,WAAW,UAAU,IAAI,KAAK,MAAM,OAC5D;GACA,WAAW,WAAW,eAAe,KAAK,OAAO;GACjD,WAAW,iBAAiB,eAAe;IACzC,WAAgB,OAAO,eAAe,CAAC,CACpC,MAAM,WAAW;KAChB,IAAI,OAAO,UAAU,KAAA,GAAW;MAC9B,IAAI,cAAc;MAClB,IAAI,OAAO,GAAG,OAAO,cAAc,GAAG,KAAK,aAAa,IAAI,OAAO,OAAO,CAAC;MAC3E;KACF;KAGA,IAAI,cAAc;KAClB,IAAI,OAAO,GAAG,QAAQ,mBAAmB,KAAK,cAAc,CAAC;KAC7D,iBAAsB,OAAO,OAAO,eAAe;IACrD,CAAC,CAAC,CACD,OAAO,QAAiB;KACvB,IAAI,cAAc;KAClB,IAAI,OACF,GAAG,OAAO,cAAc,GAAG,KAAK,aAAa,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,GAAG,CACrG;IACF,CAAC;GACL,CAAC;GACD,IAAI,OAAO,UAAU;EACvB;;;;EAKA,SAAS,iBAAiB,OAAwB,YAA0B;GAC1E,IAAI,WAAW;GACf,MAAM,aAAmB;IACvB,MACG,QAAQ,CAAC,CACT,MAAM,WAAW;KAChB,IAAI,MAAM,WAAW,GAAG;KACxB,IAAI,WAAW,QAAQ,OAAO,eAAe,SAAS,YAAY,GAAG;IACvE,CAAC,CAAC,CACD,YAAY,KAAA,CAAS,CAAC,CACtB,cAAc;KACb,YAAY;KACZ,IAAI,WAAW,IACb,OAAO,WAAW,MAAM,GAAI;IAEhC,CAAC;GACL;GACA,KAAK;EACP;;;;;;;;;;;;;;;;;;;;EChRA,MAAa,wBAAwB;EACrC,MAAa,yBAAyB;;EAGtC,MAAM,YAAY;;EAGlB,MAAM,YAAoC;GACxC,OAAO;GACP,WAAW;GACX,YAAY;GACZ,YAAY;EACd;;EAGA,SAAS,UAAU,MAAkE;GACnF,OAAO,SAAS,OACZ;IAAE,OAAO,KAAK,GAAG;IAAY,SAAS,KAAK,GAAG;GAAa,IAC3D;IAAE,OAAO,KAAK,GAAG;IAAY,SAAS,KAAK,GAAG;GAAa;EACjE;;EAGA,MAAa,cACb;;;;;;;;EASA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EvB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiLtB,MAAM,eAAe;;EAGrB,SAAgB,qBAA2B;GACzC,IAAI,OAAO,aAAa,aAAa;GACrC,IAAI,SAAS,cAAc,SAAS,aAAa,EAAE,MAAM,MAAM;GAC/D,MAAM,MAAM,SAAS,cAAc,OAAO;GAC1C,IAAI,QAAQ,YAAY;GACxB,IAAI,cAAc,GAAG,eAAe,IAAI;GACxC,SAAS,KAAK,YAAY,GAAG;EAC/B;;EASA,IAAM,cAAN,MAA6C;GAC3C,UAA6C;GAC7C,iBAAyB;GACzB,4BAA6B,IAAI,IAAgB;GACjD,eAAsC;GAEtC,SAAoC;IAClC,OAAO,KAAK;GACd;GAEA,aAAsB;IACpB,OAAO,KAAK;GACd;GAEA,UAAU,UAAkC;IAC1C,KAAK,UAAU,IAAI,QAAQ;IAC3B,aAAa;KACX,KAAK,UAAU,OAAO,QAAQ;IAChC;GACF;;;;GAKA,aAAqB;IACnB,MAAM,SAAS,KAAK;IACpB,IAAI,WAAW,MAAM,OAAO;IAC5B,OAAO,OAAO,MAAM,QACjB,SAAS,KAAK,UAAU,YAAY,KAAK,UAAU,SACtD,CAAC,CAAC;GACJ;GAEA,OAAqB;IACnB,KAAK,MAAM,YAAY,CAAC,GAAG,KAAK,SAAS,GACvC,IAAI;KACF,SAAS;IACX,QAAQ,CAER;GAEJ;GAEA,MAAM,UAA8C;IAClD,IAAI;KACF,MAAM,SAAS,MAAM,YAAY;KACjC,KAAK,UAAU;KACf,KAAK,KAAK;KACV,OAAO;IACT,QAAQ;KACN,OAAO,KAAK;IACd;GACF;GAEA,MAAM,QAA4C;IAChD,IAAI;KACF,MAAM,SAAS,MAAM,aAAa;KAClC,KAAK,UAAU;KACf,KAAK,KAAK;KACV,OAAO;IACT,QAAQ;KACN,OAAO,KAAK;IACd;GACF;;GAGA,cAAc,MAAqB;IACjC,KAAK,iBAAiB;IACtB,KAAK,KAAK;GACZ;;GAGA,mBAAyB;IACvB,IAAI,KAAK,iBAAiB,MAAM;IAChC,KAAK,eAAe,OAAO,kBAAkB;KAC3C,KAAU,QAAQ;IACpB,GAAG,GAAM;GACX;GAEA,OAAa;IACX,IAAI,KAAK,iBAAiB,MAAM;KAC9B,OAAO,cAAc,KAAK,YAAY;KACtC,KAAK,eAAe;IACtB;IACA,KAAK,UAAU,MAAM;GACvB;EACF;;;;;;EAOA,MAAa,SAAmB,CAAC;;;;;;EAOjC,SAAgB,MAAM,KAAgC;GACpD,IAAI,OAAO,aAAa,aAAa,aAAa,CAAC;GACnD,mBAAmB;GAEnB,MAAM,QAAQ,IAAI,YAAY;GAC9B,MAAM,OAAO;IAAE,IAAI,KAAK;IAAI,IAAI,KAAK;GAAG;GACxC,MAAM,gBAAiC,WAAW,OAAO,KAAK,KAAK,KAAK;GAIxE,IAAI,SAAiC,iBAAiB;GACtD,MAAM,oCAAoB,IAAI,IAAgB;GAC9C,MAAM,oBAAoB,aAAuC;IAC/D,kBAAkB,IAAI,QAAQ;IAC9B,aAAa;KACX,kBAAkB,OAAO,QAAQ;IACnC;GACF;GACA,MAAM,oBAAoB,mBAAmB,SAAS;IACpD,SAAS;IACT,KAAK,MAAM,MAAM,CAAC,GAAG,iBAAiB,GACpC,IAAI;KACF,GAAG;IACL,QAAQ,CAER;GAEJ,CAAC;GACD,MAAM,aAAa;IAAE,OAAO,UAAU,MAAM,CAAC,CAAC;IAAO,SAAS,UAAU,MAAM,CAAC,CAAC;GAAQ;GAOxF,MAAM,iBAAuB;IAE3B,MAAW,MAAM,CAAC,CAAC,MAAM,WAAW;KAClC,IAAI,WAAW,MAAM,eAAe,OAAO,QAAQ,CAAC;IACtD,CAAC;GACH;GAEA,MAAM,eAAe,kBAAkB;IACrC,cAAc;IACd,aAAa;IACb,QAAQ;IACR,MAAM;IACN,KAAK;IACL,aAAa,WAAW;IACxB,eAAe,WAAW;IAC1B,SAAS,EAAE,WAAW,iBAAiB;IACvC,OAAO;KACL,YAAY,aAAa,MAAM,UAAU,QAAQ;KACjD,aAAa,MAAM,WAAW;IAChC;IACA,UAAU;IACV,UAAU;IACV,iBAAiB;GACnB,CAAC;GAED,MAAM,iBAAiB;GACvB,MAAW,QAAQ;GAEnB,MAAM,gBAAsB;IAC1B,IAAI;KACF,aAAa;IACf,QAAQ,CAER;IACA,kBAAkB;IAClB,MAAM,KAAK;GACb;GACA,IAAI,OAAO,IAAI,WAAW,YACxB,IAAI,OAAO,SAAS,eAAe;GAErC,OAAO;EACT"}
|
package/lib/index.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
/** Plugin id — matches package.json. */
|
|
4
|
+
declare const name = "@huaqiu/dsh-tool-plugin-update";
|
|
5
|
+
/** Cordis services this half depends on: hq-edge bridge (REQUIRED) + the DSH
|
|
6
|
+
* webServer surface that hosts the same-origin forwarding routes. */
|
|
7
|
+
declare const inject: readonly ["hqEdge", "webServer"];
|
|
8
|
+
declare module '@deepseek-ai/cordis' {
|
|
9
|
+
interface Context {
|
|
10
|
+
/**
|
|
11
|
+
* Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is
|
|
12
|
+
* the loopback HQ Edge endpoint, e.g. "http://localhost:18080". Read
|
|
13
|
+
* lazily on every request.
|
|
14
|
+
*/
|
|
15
|
+
hqEdge: {
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Host plugin body — register the forwarding routes.
|
|
22
|
+
*
|
|
23
|
+
* The HQ Edge endpoint is resolved lazily per request from `ctx.hqEdge`
|
|
24
|
+
* (edge-bridge). When the bridge is absent the plugin refuses to load: an
|
|
25
|
+
* update UI without a host would only mislead.
|
|
26
|
+
*
|
|
27
|
+
* @param ctx - real cordis context (node side).
|
|
28
|
+
* @returns disposer — unregisters the routes on plugin dispose.
|
|
29
|
+
*/
|
|
30
|
+
declare function apply(ctx: Context): () => void;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { apply, inject, name };
|
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { getLogger } from "@huaqiu/dsh-plugin-log";
|
|
2
|
+
//#region src/routes.ts
|
|
3
|
+
const UPDATE_ROUTE_PREFIX = "/api/v1/huaqiu/plugin-update";
|
|
4
|
+
/** End-to-end budget for one forwarded request, in milliseconds. */
|
|
5
|
+
const DEFAULT_FORWARD_TIMEOUT_MS = 3e4;
|
|
6
|
+
function sendJson(res, status, body) {
|
|
7
|
+
const payload = JSON.stringify(body);
|
|
8
|
+
res.writeHead(status, {
|
|
9
|
+
"content-type": "application/json; charset=utf-8",
|
|
10
|
+
"cache-control": "no-store"
|
|
11
|
+
});
|
|
12
|
+
res.end(payload);
|
|
13
|
+
}
|
|
14
|
+
/** Split the sub-path off the prefix. Returns null on a bad shape. */
|
|
15
|
+
function parsePath(req) {
|
|
16
|
+
const url = req.url ?? "";
|
|
17
|
+
const q = url.indexOf("?");
|
|
18
|
+
const pathname = q >= 0 ? url.slice(0, q) : url;
|
|
19
|
+
if (!pathname.startsWith("/api/v1/huaqiu/plugin-update")) return null;
|
|
20
|
+
return pathname.slice(28);
|
|
21
|
+
}
|
|
22
|
+
/** Read the request body (bounded) as UTF-8 text. */
|
|
23
|
+
function readBody(req) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let size = 0;
|
|
26
|
+
const chunks = [];
|
|
27
|
+
req.on("data", (chunk) => {
|
|
28
|
+
size += chunk.length;
|
|
29
|
+
if (size > 65536) {
|
|
30
|
+
reject(/* @__PURE__ */ new Error("request body too large"));
|
|
31
|
+
req.destroy();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
});
|
|
36
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
37
|
+
req.on("error", reject);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Map a sub-path to { method, path } on the hq-edge API. */
|
|
41
|
+
function routeFor(sub) {
|
|
42
|
+
if (sub === "/report" || sub === "") return {
|
|
43
|
+
method: "GET",
|
|
44
|
+
path: "/api/v1/plugin-updates"
|
|
45
|
+
};
|
|
46
|
+
if (sub === "/runtime") return {
|
|
47
|
+
method: "GET",
|
|
48
|
+
path: "/api/v1/runtime-update"
|
|
49
|
+
};
|
|
50
|
+
if (sub === "/check") return {
|
|
51
|
+
method: "POST",
|
|
52
|
+
path: "/api/v1/plugin-updates/check"
|
|
53
|
+
};
|
|
54
|
+
if (sub === "/install") return {
|
|
55
|
+
method: "POST",
|
|
56
|
+
path: "/api/v1/plugin-updates/install"
|
|
57
|
+
};
|
|
58
|
+
if (sub === "/ignore") return {
|
|
59
|
+
method: "POST",
|
|
60
|
+
path: "/api/v1/plugin-updates/ignore"
|
|
61
|
+
};
|
|
62
|
+
if (sub === "/unignore") return {
|
|
63
|
+
method: "POST",
|
|
64
|
+
path: "/api/v1/plugin-updates/unignore"
|
|
65
|
+
};
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Create the prefix-route handler forwarding to hq-edge. */
|
|
69
|
+
function createUpdateForwarder(baseUrlResolver, timeoutMs = DEFAULT_FORWARD_TIMEOUT_MS) {
|
|
70
|
+
return async (req, res) => {
|
|
71
|
+
const sub = parsePath(req);
|
|
72
|
+
if (sub === null) {
|
|
73
|
+
sendJson(res, 404, { error: "not found" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const target = routeFor(sub);
|
|
77
|
+
if (target === null) {
|
|
78
|
+
sendJson(res, 404, { error: "not found" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (req.method !== "GET" && req.method !== "POST" && req.method !== "HEAD") {
|
|
82
|
+
sendJson(res, 405, { error: "method not allowed" });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const base = (baseUrlResolver() ?? "").replace(/\/+$/, "");
|
|
86
|
+
if (base.length === 0) {
|
|
87
|
+
sendJson(res, 503, { error: "hq-edge is not reachable from this plugin (no base URL)" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let body = "";
|
|
91
|
+
if (req.method === "POST") try {
|
|
92
|
+
body = await readBody(req);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
sendJson(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
99
|
+
try {
|
|
100
|
+
const upstream = await fetch(`${base}${target.path}`, {
|
|
101
|
+
method: target.method,
|
|
102
|
+
headers: {
|
|
103
|
+
"content-type": "application/json; charset=utf-8",
|
|
104
|
+
"user-agent": "hq-edge-dsh-tool-plugin-update/0.5.21"
|
|
105
|
+
},
|
|
106
|
+
...body.length > 0 ? { body } : {},
|
|
107
|
+
signal: controller.signal
|
|
108
|
+
});
|
|
109
|
+
const upstreamBody = await upstream.text();
|
|
110
|
+
let json = null;
|
|
111
|
+
try {
|
|
112
|
+
json = JSON.parse(upstreamBody);
|
|
113
|
+
} catch {
|
|
114
|
+
json = { raw: upstreamBody.slice(0, 512) };
|
|
115
|
+
}
|
|
116
|
+
sendJson(res, upstream.status, json);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
119
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
120
|
+
sendJson(res, 504, { error: `hq-edge request timed out after ${timeoutMs}ms` });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
sendJson(res, 502, { error: `hq-edge request failed: ${message}` });
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/index.ts
|
|
131
|
+
/** Plugin id — matches package.json. */
|
|
132
|
+
const name = "@huaqiu/dsh-tool-plugin-update";
|
|
133
|
+
/** Cordis services this half depends on: hq-edge bridge (REQUIRED) + the DSH
|
|
134
|
+
* webServer surface that hosts the same-origin forwarding routes. */
|
|
135
|
+
const inject = ["hqEdge", "webServer"];
|
|
136
|
+
/** Shared component name for the unified DSH-plugin log. */
|
|
137
|
+
const COMPONENT = "dsh-tool-plugin-update";
|
|
138
|
+
let _log = null;
|
|
139
|
+
function log() {
|
|
140
|
+
if (_log === null) _log = getLogger(COMPONENT);
|
|
141
|
+
return _log;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Host plugin body — register the forwarding routes.
|
|
145
|
+
*
|
|
146
|
+
* The HQ Edge endpoint is resolved lazily per request from `ctx.hqEdge`
|
|
147
|
+
* (edge-bridge). When the bridge is absent the plugin refuses to load: an
|
|
148
|
+
* update UI without a host would only mislead.
|
|
149
|
+
*
|
|
150
|
+
* @param ctx - real cordis context (node side).
|
|
151
|
+
* @returns disposer — unregisters the routes on plugin dispose.
|
|
152
|
+
*/
|
|
153
|
+
function apply(ctx) {
|
|
154
|
+
const hq = ctx.hqEdge;
|
|
155
|
+
if (!hq || typeof hq.baseUrl !== "string" || hq.baseUrl.trim().length === 0) throw new Error("@huaqiu/dsh-tool-plugin-update requires a usable hq-edge context: the edge-bridge plugin did not provide ctx.hqEdge.baseUrl. The update UI cannot work without hq-edge — check that the bridge started.");
|
|
156
|
+
const getHqEdgeBaseUrl = () => {
|
|
157
|
+
const current = ctx.hqEdge;
|
|
158
|
+
return current?.baseUrl && current.baseUrl.trim().length > 0 ? current.baseUrl : void 0;
|
|
159
|
+
};
|
|
160
|
+
const disposers = [];
|
|
161
|
+
if (ctx.webServer && typeof ctx.webServer.register === "function") ctx.effect(() => ctx.webServer.register({
|
|
162
|
+
kind: "prefix",
|
|
163
|
+
path: UPDATE_ROUTE_PREFIX,
|
|
164
|
+
handler: createUpdateForwarder(getHqEdgeBaseUrl)
|
|
165
|
+
}));
|
|
166
|
+
else log().warn("webServer unavailable — plugin-update routes are not exposed; the sidebar UI cannot load data");
|
|
167
|
+
log().info("applying dsh-tool-plugin-update node half", {
|
|
168
|
+
routePrefix: UPDATE_ROUTE_PREFIX,
|
|
169
|
+
hqEdgeBaseUrl: getHqEdgeBaseUrl() ?? null
|
|
170
|
+
});
|
|
171
|
+
return () => {
|
|
172
|
+
for (const dispose of disposers) try {
|
|
173
|
+
dispose();
|
|
174
|
+
} catch {}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
export { apply, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-tool-plugin-update",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.5.21",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./lib/index.mjs",
|
|
6
|
+
"types": "./lib/index.d.mts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.mts",
|
|
10
|
+
"default": "./lib/index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"default": "./lib/client.js"
|
|
14
|
+
},
|
|
15
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"dsh": {
|
|
19
|
+
"bundle": {
|
|
20
|
+
"patch": "./cordis.patch.yml"
|
|
21
|
+
},
|
|
22
|
+
"client": {
|
|
23
|
+
"platform": "web",
|
|
24
|
+
"inject": [
|
|
25
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
31
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@huaqiu/dsh-plugin-log": "0.5.21",
|
|
35
|
+
"@huaqiu/dsh-sidebar-entry": "0.5.21"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"lib",
|
|
39
|
+
"src",
|
|
40
|
+
"cordis.patch.yml"
|
|
41
|
+
],
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"test": "vitest run"
|
|
52
|
+
}
|
|
6
53
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin-update API client (browser half).
|
|
3
|
+
*
|
|
4
|
+
* Every call is a same-origin fetch against the DSH webServer prefix route
|
|
5
|
+
* (`/api/v1/huaqiu/plugin-update/…`) which the node half forwards to hq-edge.
|
|
6
|
+
* Response shapes mirror hq-edge `apps/server/src/routes/plugin-updates.ts`.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const UPDATE_BASE = '/api/v1/huaqiu/plugin-update'
|
|
12
|
+
|
|
13
|
+
export interface PluginUpdateItem {
|
|
14
|
+
name: string
|
|
15
|
+
currentVersion: string
|
|
16
|
+
availableVersion: string
|
|
17
|
+
state: 'current' | 'update' | 'install' | 'blocked' | 'ignored'
|
|
18
|
+
blockedBy?: string[]
|
|
19
|
+
ignored?: boolean
|
|
20
|
+
reasons?: string[]
|
|
21
|
+
notes?: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RuntimeUpdateInfo {
|
|
25
|
+
configured: boolean
|
|
26
|
+
currentVersion?: string
|
|
27
|
+
latest?: string
|
|
28
|
+
updateAvailable: boolean
|
|
29
|
+
checkedAt?: string
|
|
30
|
+
lastError?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PluginUpdateReport {
|
|
34
|
+
manifestVersion: string
|
|
35
|
+
pluginSetVersion?: string
|
|
36
|
+
items: PluginUpdateItem[]
|
|
37
|
+
actionableCount: number
|
|
38
|
+
ignoredSet: boolean
|
|
39
|
+
fetchedAt?: string
|
|
40
|
+
lastError?: string
|
|
41
|
+
runtime: RuntimeUpdateInfo
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
45
|
+
const res = await fetch(`${UPDATE_BASE}${path}`, {
|
|
46
|
+
cache: 'no-store',
|
|
47
|
+
...init,
|
|
48
|
+
headers: { 'content-type': 'application/json; charset=utf-8', ...(init?.headers ?? {}) },
|
|
49
|
+
})
|
|
50
|
+
const body = (await res.json().catch(() => null)) as T | null
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
const err = (body as { error?: string } | null)?.error ?? `HTTP ${res.status}`
|
|
53
|
+
throw new Error(err)
|
|
54
|
+
}
|
|
55
|
+
if (body === null) throw new Error('empty response')
|
|
56
|
+
return body
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** GET the cached (or empty-shell) plugin-update report. */
|
|
60
|
+
export function fetchReport(): Promise<PluginUpdateReport> {
|
|
61
|
+
return request<PluginUpdateReport>('/report')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** POST an explicit refresh; returns the freshly computed report. */
|
|
65
|
+
export function checkUpdates(): Promise<PluginUpdateReport> {
|
|
66
|
+
return request<PluginUpdateReport>('/check', { method: 'POST' })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface InstallResult {
|
|
70
|
+
ok: boolean
|
|
71
|
+
installedSetVersion?: string
|
|
72
|
+
installed?: number
|
|
73
|
+
skipped?: number
|
|
74
|
+
restarting?: boolean
|
|
75
|
+
error?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** POST the install transaction for the confirmed set version. */
|
|
79
|
+
export function installSet(setVersion: string): Promise<InstallResult> {
|
|
80
|
+
return request<InstallResult>('/install', {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
body: JSON.stringify({ setVersion }),
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** POST ignore for the current offer (set-scoped, persists across restarts). */
|
|
87
|
+
export function ignoreOffer(setVersion: string): Promise<unknown> {
|
|
88
|
+
return request<unknown>('/ignore', { method: 'POST', body: JSON.stringify({ setVersion }) })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** POST unignore for the set version (reverses an ignore). */
|
|
92
|
+
export function unignoreOffer(setVersion: string): Promise<unknown> {
|
|
93
|
+
return request<unknown>('/unignore', { method: 'POST', body: JSON.stringify({ setVersion }) })
|
|
94
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Localized copy for the plugin-update UI (browser half).
|
|
3
|
+
*
|
|
4
|
+
* zh is the fallback, matching the rest of the Huaqiu plugin family
|
|
5
|
+
* (`<html lang>` drives the resolution via the shared sidebar-entry locale
|
|
6
|
+
* helpers).
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const zh = {
|
|
12
|
+
entryLabel: '插件更新',
|
|
13
|
+
entryTooltip: '查看可用的插件更新',
|
|
14
|
+
viewTitle: '插件更新',
|
|
15
|
+
refresh: '刷新',
|
|
16
|
+
close: '关闭',
|
|
17
|
+
empty: '没有可用的插件更新',
|
|
18
|
+
checkFailed: '检查更新失败',
|
|
19
|
+
manifestVersion: '插件集',
|
|
20
|
+
ignoredOffer: '已忽略当前插件集更新',
|
|
21
|
+
unignore: '恢复提醒',
|
|
22
|
+
installAll: (count: number) => `安装 ${count} 项更新`,
|
|
23
|
+
installing: '安装中…(DSH 将自动重启)',
|
|
24
|
+
state: {
|
|
25
|
+
current: '当前',
|
|
26
|
+
update: '可更新',
|
|
27
|
+
install: '待安装',
|
|
28
|
+
ignored: '已忽略',
|
|
29
|
+
blocked: '被阻止',
|
|
30
|
+
},
|
|
31
|
+
blockedReason: {
|
|
32
|
+
host: '不适用于当前 EDA 主机',
|
|
33
|
+
'hq-edge-version': '当前 HQ Edge 版本不兼容',
|
|
34
|
+
'runtime-unknown': 'HQ Edge 版本未知,无法校验',
|
|
35
|
+
},
|
|
36
|
+
runtime: {
|
|
37
|
+
title: 'HQ Edge 运行时',
|
|
38
|
+
updateAvailable: (version: string) => `发现新版本 ${version}`,
|
|
39
|
+
current: (version: string) => `当前版本 ${version}`,
|
|
40
|
+
unknown: '版本未知',
|
|
41
|
+
notConfigured: '未配置更新源',
|
|
42
|
+
note: '运行时更新由 EDA 安装器处理',
|
|
43
|
+
},
|
|
44
|
+
lastError: '上次检查失败',
|
|
45
|
+
fetchedAt: '检查时间',
|
|
46
|
+
installError: '安装失败',
|
|
47
|
+
installSuccess: '更新已安装,正在重启 DSH…',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type UpdateCopy = typeof zh
|
|
51
|
+
|
|
52
|
+
const en: UpdateCopy = {
|
|
53
|
+
entryLabel: 'Plugin Updates',
|
|
54
|
+
entryTooltip: 'Review available plugin updates',
|
|
55
|
+
viewTitle: 'Plugin Updates',
|
|
56
|
+
refresh: 'Refresh',
|
|
57
|
+
close: 'Close',
|
|
58
|
+
empty: 'No plugin updates available',
|
|
59
|
+
checkFailed: 'Failed to check for updates',
|
|
60
|
+
manifestVersion: 'Plugin set',
|
|
61
|
+
ignoredOffer: 'The current plugin-set update is ignored',
|
|
62
|
+
unignore: 'Show again',
|
|
63
|
+
installAll: (count) => `Install ${count} update${count === 1 ? '' : 's'}`,
|
|
64
|
+
installing: 'Installing… (DSH will restart)',
|
|
65
|
+
state: {
|
|
66
|
+
current: 'Current',
|
|
67
|
+
update: 'Update',
|
|
68
|
+
install: 'Install',
|
|
69
|
+
ignored: 'Ignored',
|
|
70
|
+
blocked: 'Blocked',
|
|
71
|
+
},
|
|
72
|
+
blockedReason: {
|
|
73
|
+
host: 'Not applicable to the current EDA host',
|
|
74
|
+
'hq-edge-version': 'Incompatible with the current HQ Edge version',
|
|
75
|
+
'runtime-unknown': 'HQ Edge version unknown — cannot verify',
|
|
76
|
+
},
|
|
77
|
+
runtime: {
|
|
78
|
+
title: 'HQ Edge Runtime',
|
|
79
|
+
updateAvailable: (version) => `New version ${version} available`,
|
|
80
|
+
current: (version) => `Current version ${version}`,
|
|
81
|
+
unknown: 'Version unknown',
|
|
82
|
+
notConfigured: 'No update source configured',
|
|
83
|
+
note: 'Runtime updates are handled by the EDA installer',
|
|
84
|
+
},
|
|
85
|
+
lastError: 'Last check failed',
|
|
86
|
+
fetchedAt: 'Checked at',
|
|
87
|
+
installError: 'Install failed',
|
|
88
|
+
installSuccess: 'Update installed — restarting DSH…',
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const COPY = { zh, en } as const
|
|
92
|
+
|