@dshtrading/client-ui-updater 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +25 -26
- package/lib/client.js.map +1 -1
- package/lib/github.js +45 -2
- package/lib/updater-service.d.ts +7 -0
- package/lib/updater-service.js +30 -5
- package/package.json +9 -10
package/lib/client.js
CHANGED
|
@@ -130,7 +130,7 @@ window.__ModuleLoader__.load({
|
|
|
130
130
|
}
|
|
131
131
|
//#endregion
|
|
132
132
|
//#region \0dsh-css:/home/runner/work/dsh-trading/dsh-trading/packages/client-ui-updater/src/client/updater-panel.module.css.mjs
|
|
133
|
-
const css$1 = ".SgPada_root{--upd-text-primary:var(--dsw-alias-text-primary,#1a1e24);--upd-text-secondary:var(--dsw-alias-text-secondary,#5f6672);--upd-text-muted:var(--dsw-alias-text-muted,#8e95a3);--upd-border:var(--dsw-alias-border-l2,#e3e6ea);--upd-bg-hover:var(--dsw-alias-fill-hover,#0000000a);--upd-bg-active:var(--dsw-alias-primary-bg,#2563eb0f);--upd-primary:var(--dsw-alias-primary,#2563eb);--upd-ok:#16a34a;--upd-danger:#dc2626;color:var(--upd-text-primary);flex-direction:column;gap:16px;padding:0;font-size:13px;display:flex}body[data-ds-dark-theme] .SgPada_root,[data-theme=dark] .SgPada_root,:root[data-theme=dark] .SgPada_root,html.dark .SgPada_root,body.dark .SgPada_root{--upd-text-primary:var(--dsw-alias-text-primary,#f3f4f6);--upd-text-secondary:var(--dsw-alias-text-secondary,#9ca3af);--upd-text-muted:var(--dsw-alias-text-muted,#6b7280);--upd-border:var(--dsw-alias-border-l2,#ffffff1a);--upd-bg-hover:var(--dsw-alias-fill-hover,#ffffff0f);--upd-bg-active:#2563eb29;--upd-primary:#3b82f6;--upd-ok:#4ade80;--upd-danger:#f87171}.SgPada_lead{color:var(--upd-text-secondary);margin:0;font-size:13.5px;line-height:1.6}.SgPada_versions{border:1px solid var(--upd-border);background:var(--upd-bg-hover);border-radius:8px;flex-wrap:wrap;gap:6px 24px;padding:10px 12px;display:flex}.SgPada_versionItem{flex-direction:column;gap:2px;display:flex}.SgPada_versionLabel{color:var(--upd-text-muted);font-size:11.5px}.SgPada_versionValue{font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}.SgPada_statusRow{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.SgPada_statusText{color:var(--upd-text-secondary)}.SgPada_statusTextOk{color:var(--upd-ok)}.
|
|
133
|
+
const css$1 = ".SgPada_root{--upd-text-primary:var(--dsw-alias-text-primary,#1a1e24);--upd-text-secondary:var(--dsw-alias-text-secondary,#5f6672);--upd-text-muted:var(--dsw-alias-text-muted,#8e95a3);--upd-border:var(--dsw-alias-border-l2,#e3e6ea);--upd-bg-hover:var(--dsw-alias-fill-hover,#0000000a);--upd-bg-active:var(--dsw-alias-primary-bg,#2563eb0f);--upd-primary:var(--dsw-alias-primary,#2563eb);--upd-ok:#16a34a;--upd-danger:#dc2626;color:var(--upd-text-primary);flex-direction:column;gap:16px;padding:0;font-size:13px;display:flex}body[data-ds-dark-theme] .SgPada_root,[data-theme=dark] .SgPada_root,:root[data-theme=dark] .SgPada_root,html.dark .SgPada_root,body.dark .SgPada_root{--upd-text-primary:var(--dsw-alias-text-primary,#f3f4f6);--upd-text-secondary:var(--dsw-alias-text-secondary,#9ca3af);--upd-text-muted:var(--dsw-alias-text-muted,#6b7280);--upd-border:var(--dsw-alias-border-l2,#ffffff1a);--upd-bg-hover:var(--dsw-alias-fill-hover,#ffffff0f);--upd-bg-active:#2563eb29;--upd-primary:#3b82f6;--upd-ok:#4ade80;--upd-danger:#f87171}.SgPada_lead{color:var(--upd-text-secondary);margin:0;font-size:13.5px;line-height:1.6}.SgPada_versions{border:1px solid var(--upd-border);background:var(--upd-bg-hover);border-radius:8px;flex-wrap:wrap;gap:6px 24px;padding:10px 12px;display:flex}.SgPada_versionItem{flex-direction:column;gap:2px;display:flex}.SgPada_versionLabel{color:var(--upd-text-muted);font-size:11.5px}.SgPada_versionValue{font-variant-numeric:tabular-nums;font-size:13px;font-weight:600}.SgPada_statusRow{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.SgPada_statusText{color:var(--upd-text-secondary)}.SgPada_statusTextOk{color:var(--upd-ok)}.SgPada_statusTextError{color:var(--upd-danger)}.SgPada_availableCard{border:1px solid var(--upd-primary);background:var(--upd-bg-active);border-radius:8px;flex-direction:column;gap:10px;padding:14px;display:flex}.SgPada_availableTitle{color:var(--upd-primary);margin:0;font-size:14px;font-weight:600}.SgPada_availableMeta{color:var(--upd-text-muted);margin:0;font-size:12px}.SgPada_notesTitle{color:var(--upd-text-secondary);margin:0;font-size:12.5px;font-weight:600}.SgPada_notes{border:1px solid var(--upd-border);background:var(--upd-bg-hover);color:var(--upd-text-secondary);white-space:pre-wrap;word-break:break-word;border-radius:6px;max-height:260px;margin:0;padding:10px 12px;font-family:inherit;font-size:12.5px;line-height:1.6;overflow:auto}.SgPada_actions{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.SgPada_primaryButton{appearance:none;background:var(--upd-primary);color:#fff;cursor:pointer;border:none;border-radius:6px;padding:7px 16px;font-size:13px;font-weight:500}.SgPada_primaryButton:hover{filter:brightness(1.08)}.SgPada_primaryButton:disabled{opacity:.55;cursor:default;filter:none}.SgPada_secondaryButton{appearance:none;border:1px solid var(--upd-border);color:var(--upd-text-secondary);cursor:pointer;background:0 0;border-radius:6px;padding:6px 14px;font-size:13px}.SgPada_secondaryButton:hover{background:var(--upd-bg-hover)}.SgPada_secondaryButton:disabled{opacity:.55;cursor:default}.SgPada_link{color:var(--upd-primary);font-size:12.5px;text-decoration:none}.SgPada_link:hover{text-decoration:underline}.SgPada_progressWrap{flex-direction:column;gap:6px;display:flex}.SgPada_progressLabel{color:var(--upd-text-secondary);justify-content:space-between;font-size:12.5px;display:flex}.SgPada_progressTrack{background:var(--upd-bg-hover);border:1px solid var(--upd-border);border-radius:3px;height:6px;overflow:hidden}.SgPada_progressFill{background:var(--upd-primary);border-radius:3px;height:100%;transition:width .24s}.SgPada_progressIndeterminate{width:40%;animation:1.1s ease-in-out infinite alternate SgPada_upd-slide}@keyframes SgPada_upd-slide{0%{margin-left:0}to{margin-left:60%}}.SgPada_doneCard{border:1px solid var(--upd-ok);border-radius:8px;flex-direction:column;gap:8px;padding:14px;display:flex}.SgPada_doneTitle{color:var(--upd-ok);margin:0;font-size:14px;font-weight:600}.SgPada_doneMeta{color:var(--upd-text-secondary);margin:0;font-size:12.5px}.SgPada_errorText{color:var(--upd-danger);word-break:break-word;margin:0;font-size:12.5px}";
|
|
134
134
|
const tagId$1 = "@dshtrading/client-ui-updater/updater-panel.module.css";
|
|
135
135
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
136
136
|
const tag = document.createElement("style");
|
|
@@ -140,36 +140,35 @@ window.__ModuleLoader__.load({
|
|
|
140
140
|
document.head.appendChild(tag);
|
|
141
141
|
}
|
|
142
142
|
var updater_panel_module_css_default = {
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"secondaryButton": "SgPada_secondaryButton",
|
|
146
|
-
"progressFill": "SgPada_progressFill",
|
|
147
|
-
"root": "SgPada_root",
|
|
148
|
-
"progressTrack": "SgPada_progressTrack",
|
|
149
|
-
"progressLabel": "SgPada_progressLabel",
|
|
150
|
-
"doneCard": "SgPada_doneCard",
|
|
151
|
-
"statusRow": "SgPada_statusRow",
|
|
152
|
-
"versions": "SgPada_versions",
|
|
143
|
+
"notes": "SgPada_notes",
|
|
144
|
+
"availableMeta": "SgPada_availableMeta",
|
|
153
145
|
"availableCard": "SgPada_availableCard",
|
|
154
|
-
"
|
|
146
|
+
"doneTitle": "SgPada_doneTitle",
|
|
155
147
|
"lead": "SgPada_lead",
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
"
|
|
148
|
+
"doneMeta": "SgPada_doneMeta",
|
|
149
|
+
"versions": "SgPada_versions",
|
|
150
|
+
"progressLabel": "SgPada_progressLabel",
|
|
151
|
+
"link": "SgPada_link",
|
|
159
152
|
"availableTitle": "SgPada_availableTitle",
|
|
153
|
+
"progressTrack": "SgPada_progressTrack",
|
|
160
154
|
"actions": "SgPada_actions",
|
|
155
|
+
"root": "SgPada_root",
|
|
156
|
+
"statusRow": "SgPada_statusRow",
|
|
157
|
+
"progressFill": "SgPada_progressFill",
|
|
158
|
+
"versionLabel": "SgPada_versionLabel",
|
|
161
159
|
"notesTitle": "SgPada_notesTitle",
|
|
162
160
|
"progressWrap": "SgPada_progressWrap",
|
|
161
|
+
"statusTextError": "SgPada_statusTextError",
|
|
162
|
+
"statusTextOk": "SgPada_statusTextOk",
|
|
163
|
+
"doneCard": "SgPada_doneCard",
|
|
163
164
|
"statusText": "SgPada_statusText",
|
|
164
|
-
"doneMeta": "SgPada_doneMeta",
|
|
165
|
-
"progressIndeterminate": "SgPada_progressIndeterminate",
|
|
166
|
-
"statusTextAvailable": "SgPada_statusTextAvailable",
|
|
167
|
-
"upd-slide": "SgPada_upd-slide",
|
|
168
165
|
"errorText": "SgPada_errorText",
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
166
|
+
"primaryButton": "SgPada_primaryButton",
|
|
167
|
+
"versionValue": "SgPada_versionValue",
|
|
168
|
+
"versionItem": "SgPada_versionItem",
|
|
169
|
+
"secondaryButton": "SgPada_secondaryButton",
|
|
170
|
+
"upd-slide": "SgPada_upd-slide",
|
|
171
|
+
"progressIndeterminate": "SgPada_progressIndeterminate"
|
|
173
172
|
};
|
|
174
173
|
//#endregion
|
|
175
174
|
//#region src/client/UpdaterPanel.tsx
|
|
@@ -453,13 +452,13 @@ window.__ModuleLoader__.load({
|
|
|
453
452
|
document.head.appendChild(tag);
|
|
454
453
|
}
|
|
455
454
|
var updater_dialog_module_css_default = {
|
|
456
|
-
"
|
|
455
|
+
"header": "Sd___q_header",
|
|
457
456
|
"title": "Sd___q_title",
|
|
458
|
-
"close": "Sd___q_close",
|
|
459
457
|
"body": "Sd___q_body",
|
|
458
|
+
"close": "Sd___q_close",
|
|
460
459
|
"mask": "Sd___q_mask",
|
|
461
460
|
"panel": "Sd___q_panel",
|
|
462
|
-
"
|
|
461
|
+
"overlay": "Sd___q_overlay"
|
|
463
462
|
};
|
|
464
463
|
//#endregion
|
|
465
464
|
//#region src/client/UpdaterDialog.tsx
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["useState","useRef","useCallback","css","useState","useCallback","css"],"sources":["../src/client/locales.ts","../src/client/contract.ts","../src/client/api.ts","../src/client/UpdaterPanel.tsx","../src/client/UpdaterDialog.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * client-ui-updater 词典(纯数据模块:零运行时依赖)。\n *\n * 单一来源:本包 client apply 注册 zh/en(typed register 编译期校验键位);\n * packages/dsh-i18n 语言包构建期 import 本模块注册 zh-CN;scripts/i18n-audit.mjs\n * 静态加载本模块做 zh/en 键对齐与占位符对齐门禁。\n */\nimport type { UpdaterLocaleKey } from './contract.ts'\n\nexport const zh: Record<UpdaterLocaleKey, string> = {\n nav: '软件更新',\n lead: '检测 GitHub Releases 上的新版本,增量更新只下载变动的交易插件包,无需重装应用。',\n currentVersion: '交易插件版本',\n desktopApp: '桌面应用版本',\n unsupported: '当前环境不支持自动增量更新(仅 DSH Trading 桌面版支持)。可以前往发布页手动下载完整安装包。',\n viewReleases: '在 GitHub 查看发布',\n checkNow: '检查更新',\n checking: '正在检查…',\n lastCheck: '上次检查',\n never: '尚未检查',\n upToDate: '已是最新版本。',\n available: '发现新版本 {version}',\n publishedAt: '发布于 {date}',\n notesTitle: '更新说明',\n applyNow: '立即更新',\n payloadMissing: '该版本未附带增量更新包,请前往发布页下载完整安装包。',\n 'phase.prepare': '正在准备…',\n 'phase.download': '正在下载增量包…',\n 'phase.verify': '正在校验增量包…',\n 'phase.install': '正在安装更新…',\n progress: '{percent}%',\n done: '已更新到 {version}',\n restartHint: '重启 DSH Trading 后生效。',\n restartNow: '重启应用',\n restartManual: '请手动重启 DSH Trading 以完成更新。',\n checkError: '检查失败:{message}',\n retry: '重试',\n close: '关闭',\n}\n\nexport const en: Record<UpdaterLocaleKey, string> = {\n nav: 'Software updates',\n lead: 'Detects new versions published on GitHub Releases and applies incremental updates — only the changed trading plugin packages are downloaded, no reinstall needed.',\n currentVersion: 'Trading plugins',\n desktopApp: 'Desktop app',\n unsupported: 'Incremental updates are only available inside the DSH Trading desktop app. You can download a full installer from the releases page.',\n viewReleases: 'View releases on GitHub',\n checkNow: 'Check for updates',\n checking: 'Checking…',\n lastCheck: 'Last checked',\n never: 'Never checked',\n upToDate: 'You are up to date.',\n available: 'New version {version} available',\n publishedAt: 'Published {date}',\n notesTitle: 'Release notes',\n applyNow: 'Update now',\n payloadMissing: 'This release does not carry the incremental update payload. Please download a full installer from the releases page.',\n 'phase.prepare': 'Preparing…',\n 'phase.download': 'Downloading update payload…',\n 'phase.verify': 'Verifying update payload…',\n 'phase.install': 'Installing update…',\n progress: '{percent}%',\n done: 'Updated to {version}',\n restartHint: 'Restart DSH Trading to activate.',\n restartNow: 'Restart app',\n restartManual: 'Please restart DSH Trading manually to finish the update.',\n checkError: 'Check failed: {message}',\n retry: 'Retry',\n close: 'Close',\n}\n","/**\n * Locale contract for the software-update dialog. The zh dictionary in\n * locales.ts is the key source of truth; this union + the LocaleNamespaceMap\n * merge give the framework-injected t seat its key types.\n *\n * Type-only imports pull the host slot augmentations (shell.overlay) into the\n * program so the dialog registers against a declared slot.\n */\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\n\nexport type UpdaterLocaleKey =\n | 'nav'\n | 'lead'\n | 'currentVersion'\n | 'desktopApp'\n | 'unsupported'\n | 'viewReleases'\n | 'checkNow'\n | 'checking'\n | 'lastCheck'\n | 'never'\n | 'upToDate'\n | 'available'\n | 'publishedAt'\n | 'notesTitle'\n | 'applyNow'\n | 'payloadMissing'\n | 'phase.prepare'\n | 'phase.download'\n | 'phase.verify'\n | 'phase.install'\n | 'progress'\n | 'done'\n | 'restartHint'\n | 'restartNow'\n | 'restartManual'\n | 'checkError'\n | 'retry'\n | 'close'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Software-update dialog copy (check / notes / incremental apply). */\n 'dshtrading.updater': UpdaterLocaleKey\n }\n}\n\n/**\n * Window event contract with the trading shell sidebar (client-ui-trading\n * MarketDock update entry). Cross-plugin coupling is intentionally a DOM\n * custom event, not an import (client plugins never import each other's\n * modules).\n *\n * - UPDATE_AVAILABLE_EVENT: fired by the dialog whenever the host snapshot\n * changes. Detail: { available: boolean, version?: string }.\n * - UPDATER_OPEN_EVENT: fired by the sidebar update entry to ask this plugin\n * to open its dialog. No detail.\n */\nexport const UPDATE_AVAILABLE_EVENT = 'dshtrading-update-available'\nexport const UPDATER_OPEN_EVENT = 'dshtrading-updater-open'\n","/**\n * Updater bridge client: same-origin fetch wrappers over\n * /dshtrading/api/updater (the node half registers the route behind the\n * browser-auth fence; same-origin fetch carries the auth cookie by default).\n * Error semantics mirror the tasks bridge: non-2xx or { ok:false } envelopes\n * become rejections carrying the business code.\n */\nimport type { UpdaterSnapshot } from '../updater-service.ts'\n\nexport class UpdaterBridgeError extends Error {\n constructor(readonly status: number, message: string, readonly code?: string) {\n super(message)\n }\n}\n\nasync function updaterJson<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await fetch(path, init)\n if (!response.ok) {\n const body = await response.json().catch(() => undefined) as { code?: string; message?: string } | undefined\n const detail = typeof body?.message === 'string' && body.message !== '' ? ': ' + body.message : ''\n throw new UpdaterBridgeError(response.status, 'updater ' + path + ' failed: ' + String(response.status) + detail, body?.code)\n }\n const wire = await response.json() as T\n if (wire !== null && typeof wire === 'object' && (wire as { ok?: unknown }).ok === false) {\n const business = wire as { code?: string; message?: string }\n throw new UpdaterBridgeError(200, (business.code ?? 'UPDATER_UNKNOWN') + ': ' + (business.message ?? 'updater bridge error'), business.code)\n }\n return wire\n}\n\n/** Current updater snapshot (environment + check + apply state). */\nexport async function fetchUpdaterState(): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>('/dshtrading/api/updater/state', { headers: { accept: 'application/json' } })\n}\n\n/** Run a GitHub release check; force=true bypasses the host-side TTL cache. */\nexport async function requestUpdaterCheck(force: boolean): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>(`/dshtrading/api/updater/check?force=${force ? '1' : '0'}`, { headers: { accept: 'application/json' } })\n}\n\n/** Start the incremental apply pipeline; returns the running snapshot. */\nexport async function requestUpdaterApply(): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>('/dshtrading/api/updater/apply', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: '{}',\n })\n}\n\n/** Electron shell bridge exposed by the desktop preload (absent in browsers). */\ninterface DesktopBridge {\n relaunch?: () => void\n}\n\n/** Resolve the desktop shell bridge, if this page runs inside DSH Trading. */\nexport function desktopBridge(): DesktopBridge | undefined {\n if (typeof window === 'undefined' || !('desktop' in window)) return undefined\n return (window as { desktop?: DesktopBridge }).desktop\n}\n","/**\n * Software-update dialog body (rendered inside UpdaterDialog). Pure view over\n * /dshtrading/api/updater: renders the current versions, the latest GitHub\n * release with its notes, and drives the incremental apply + restart flow.\n * Polls the host snapshot while mounted (faster while an apply is running);\n * no SSE dependency.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { UpdaterSnapshot } from '../updater-service.ts'\nimport { desktopBridge, fetchUpdaterState, requestUpdaterApply, requestUpdaterCheck } from './api.ts'\nimport { UPDATE_AVAILABLE_EVENT } from './contract.ts'\nimport css from './updater-panel.module.css'\n\nexport type UpdaterPanelProps = PropsLocale<'dshtrading.updater'>\n\n/** Poll cadences: lazy when idle, tight while an apply pipeline is running. */\nconst POLL_IDLE_MS = 30_000\nconst POLL_RUNNING_MS = 1_200\n\n/** ISO date (yyyy-mm-dd) for the published line; empty when unknown. */\nfunction formatDay(iso: string | undefined): string {\n if (iso === undefined) return ''\n const parsed = new Date(iso)\n if (Number.isNaN(parsed.getTime())) return ''\n return parsed.toISOString().slice(0, 10)\n}\n\nexport function UpdaterPanel({ t }: UpdaterPanelProps) {\n const [state, setState] = useState<UpdaterSnapshot | undefined>(undefined)\n const [actionError, setActionError] = useState<string | undefined>(undefined)\n const [checkBusy, setCheckBusy] = useState(false)\n const applyBusyRef = useRef(false)\n\n const running = state?.apply.phase === 'running'\n\n const refresh = useCallback(async () => {\n try {\n setState(await fetchUpdaterState())\n } catch {\n // Bridge absent (headless profile / old deployment): keep the page quiet.\n }\n }, [])\n\n useEffect(() => { void refresh() }, [refresh])\n\n useEffect(() => {\n const interval = setInterval(() => { void refresh() }, running ? POLL_RUNNING_MS : POLL_IDLE_MS)\n return () => { clearInterval(interval) }\n }, [refresh, running])\n\n // Sidebar badge sync: mirror availability onto the window event the trading\n // shell sidebar listens to (see UPDATE_AVAILABLE_EVENT in contract.ts).\n useEffect(() => {\n if (state === undefined) return\n const available = state.check.available === true && state.apply.phase !== 'done'\n window.dispatchEvent(new CustomEvent(UPDATE_AVAILABLE_EVENT, {\n detail: { available, version: state.check.latest?.version },\n }))\n }, [state])\n\n const handleCheck = async (): Promise<void> => {\n setCheckBusy(true)\n setActionError(undefined)\n try {\n setState(await requestUpdaterCheck(true))\n } catch (error) {\n setActionError(error instanceof Error ? error.message : String(error))\n } finally {\n setCheckBusy(false)\n }\n }\n\n const handleApply = async (): Promise<void> => {\n if (applyBusyRef.current) return\n applyBusyRef.current = true\n setActionError(undefined)\n try {\n setState(await requestUpdaterApply())\n // Pipeline started: tighten polling immediately instead of waiting a tick.\n void refresh()\n } catch (error) {\n setActionError(error instanceof Error ? error.message : String(error))\n } finally {\n applyBusyRef.current = false\n }\n }\n\n const handleRestart = (): void => {\n desktopBridge()?.relaunch?.()\n }\n\n const environment = state?.environment\n const check = state?.check\n const apply = state?.apply\n const progress = state?.progress\n const latest = check?.latest\n const releasesUrl = latest?.url ?? 'https://github.com/zhu1090093659/dsh-trading/releases'\n const publishedDay = formatDay(latest?.publishedAt)\n const hasUpdate = check?.available === true && apply?.phase !== 'done'\n const applyable = hasUpdate && latest?.payloadAvailable === true\n\n return (\n <div className={css.root}>\n <p className={css.lead}>{t('lead')}</p>\n\n <div className={css.versions}>\n <div className={css.versionItem}>\n <span className={css.versionLabel}>{t('currentVersion')}</span>\n <span className={css.versionValue}>{environment?.familyVersion ?? '—'}</span>\n </div>\n {environment?.appVersion !== undefined && (\n <div className={css.versionItem}>\n <span className={css.versionLabel}>{t('desktopApp')}</span>\n <span className={css.versionValue}>{environment.appVersion}</span>\n </div>\n )}\n </div>\n\n {environment !== undefined && !environment.supported && (\n <p className={css.statusText}>{t('unsupported')}</p>\n )}\n\n {environment?.supported === true && (\n <>\n <div className={css.statusRow}>\n {check?.status === 'error' && (\n <span className={`${css.statusText} ${css.statusTextError}`}>\n {t('checkError', { message: check.error ?? '' })}\n </span>\n )}\n {check?.status === 'ok' && !hasUpdate && apply?.phase !== 'done' && (\n <span className={`${css.statusText} ${css.statusTextOk}`}>{t('upToDate')}</span>\n )}\n {check?.at !== undefined && (\n <span className={css.statusText}>\n {t('lastCheck')}\n :\n {' '}\n {check.at.slice(0, 19).replace('T', ' ')}\n </span>\n )}\n {check?.at === undefined && check?.status !== 'ok' && (\n <span className={css.statusText}>{t('never')}</span>\n )}\n <button\n type=\"button\"\n className={css.secondaryButton}\n disabled={checkBusy || running}\n onClick={() => { void handleCheck() }}\n >\n {checkBusy ? t('checking') : t('checkNow')}\n </button>\n {actionError !== undefined && (\n <button type=\"button\" className={css.secondaryButton} onClick={() => { void handleCheck() }}>\n {t('retry')}\n </button>\n )}\n </div>\n\n {running && (\n <div className={css.progressWrap}>\n <span className={css.progressLabel}>\n <span>\n {progress?.step === 'download'\n ? t('phase.download')\n : progress?.step === 'verify'\n ? t('phase.verify')\n : progress?.step === 'install'\n ? t('phase.install')\n : t('phase.prepare')}\n </span>\n {progress?.percent !== undefined && (\n <span>{t('progress', { percent: progress.percent })}</span>\n )}\n </span>\n <div\n className={css.progressTrack}\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={progress?.percent ?? undefined}\n >\n <div\n className={`${css.progressFill} ${progress?.percent === undefined ? css.progressIndeterminate : ''}`}\n style={progress?.percent === undefined ? undefined : { width: `${progress.percent}%` }}\n />\n </div>\n </div>\n )}\n\n {hasUpdate && latest !== undefined && !running && (\n <div className={css.availableCard}>\n <p className={css.availableTitle}>{t('available', { version: latest.version })}</p>\n {publishedDay !== '' && (\n <p className={css.availableMeta}>{t('publishedAt', { date: publishedDay })}</p>\n )}\n {latest.notes.trim() !== '' && (\n <>\n <span className={css.notesTitle}>{t('notesTitle')}</span>\n <pre className={css.notes}>{latest.notes}</pre>\n </>\n )}\n <div className={css.actions}>\n {latest.payloadAvailable && (\n <button\n type=\"button\"\n className={css.primaryButton}\n onClick={() => { void handleApply() }}\n >\n {t('applyNow')}\n </button>\n )}\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n </div>\n {!latest.payloadAvailable && (\n <p className={css.doneMeta}>{t('payloadMissing')}</p>\n )}\n </div>\n )}\n\n {apply?.phase === 'done' && apply.targetVersion !== undefined && (\n <div className={css.doneCard}>\n <p className={css.doneTitle}>{t('done', { version: apply.targetVersion })}</p>\n <p className={css.doneMeta}>{t('restartHint')}</p>\n <div className={css.actions}>\n {desktopBridge()?.relaunch !== undefined && (\n <button type=\"button\" className={css.primaryButton} onClick={handleRestart}>\n {t('restartNow')}\n </button>\n )}\n {desktopBridge()?.relaunch === undefined && (\n <span className={css.doneMeta}>{t('restartManual')}</span>\n )}\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n </div>\n </div>\n )}\n\n {apply?.phase === 'error' && apply.error !== undefined && (\n <p className={css.errorText}>{apply.error}</p>\n )}\n </>\n )}\n\n {environment === undefined || !environment.supported ? (\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n ) : null}\n </div>\n )\n}\n","/**\n * Software-update dialog (the 'shell.overlay' entry registered by this plugin).\n * The sidebar's update entry dispatches UPDATER_OPEN_EVENT; this component owns\n * the modal chrome (mask click + Escape close) around UpdaterPanel. Frame-wide\n * floating layer: fixed overlay opts back into pointer events.\n */\nimport { useCallback, useEffect, useState } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { UPDATER_OPEN_EVENT } from './contract.ts'\nimport { UpdaterPanel } from './UpdaterPanel.tsx'\nimport css from './updater-dialog.module.css'\n\nexport type UpdaterDialogProps = PropsLocale<'dshtrading.updater'>\n\nexport function UpdaterDialog({ t }: UpdaterDialogProps) {\n const [open, setOpen] = useState(false)\n\n useEffect(() => {\n const onOpen = (): void => { setOpen(true) }\n window.addEventListener(UPDATER_OPEN_EVENT, onOpen)\n return () => { window.removeEventListener(UPDATER_OPEN_EVENT, onOpen) }\n }, [])\n\n const close = useCallback((): void => { setOpen(false) }, [])\n\n useEffect(() => {\n if (!open) return\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') close()\n }\n document.addEventListener('keydown', onKeyDown)\n return () => { document.removeEventListener('keydown', onKeyDown) }\n }, [open, close])\n\n if (!open) return null\n\n return (\n <div className={css.overlay} role=\"presentation\">\n <div className={css.mask} aria-hidden=\"true\" onClick={close} />\n <div className={css.panel} role=\"dialog\" aria-modal=\"true\" aria-label={t('nav')}>\n <div className={css.header}>\n <span className={css.title}>{t('nav')}</span>\n <button type=\"button\" className={css.close} aria-label={t('close')} title={t('close')} onClick={close}>\n <svg viewBox=\"0 0 16 16\" width={14} height={14} aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M4.25 4.25l7.5 7.5M11.75 4.25l-7.5 7.5\" />\n </svg>\n </button>\n </div>\n <div className={css.body}>\n <UpdaterPanel t={t} />\n </div>\n </div>\n </div>\n )\n}\n","/**\n * dsh-trading auto-update, browser half — the software-update dialog on the\n * host's frame-wide shell.overlay layer. The entry point is the update button\n * beside Settings in the trading sidebar's bottom bar (client-ui-trading\n * dispatches UPDATER_OPEN_EVENT); this plugin owns the modal chrome and body.\n *\n * The dialog talks to the node half over /dshtrading/api/updater (same-origin\n * fetch inside the browser-auth fence). In environments without incremental\n * support (dev checkouts, headless profiles) it degrades to an information\n * surface linking the GitHub releases page.\n */\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from './contract.ts'\nimport { en, zh } from './locales.ts'\nimport { UpdaterDialog } from './UpdaterDialog.tsx'\n\nimport { UPDATE_AVAILABLE_EVENT, UPDATER_OPEN_EVENT } from './contract.ts'\n/** This dialog's locale namespace. */\nconst NS = 'dshtrading.updater'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale']\n\n/**\n * Register the update dialog on shell.overlay (no settings.section: the entry\n * moved beside Settings in the sidebar bottom bar). The sidebar asks this\n * plugin to open through the UPDATER_OPEN_EVENT window event — cross-plugin\n * coupling is a DOM event, never an import.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.locale.bind(NS)\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-trading-updater: dictionaries')\n\n ctx.slots.inject('shell.overlay', () => ctx.slots.register({\n name: 'shell.overlay',\n id: 'dshtrading-updater-dialog',\n order: 90,\n locale: NS,\n }, UpdaterDialog))\n}\n\nexport { UPDATE_AVAILABLE_EVENT, UPDATER_OPEN_EVENT } from './contract.ts'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["useState","useRef","useCallback","css","useState","useCallback","css"],"sources":["../src/client/locales.ts","../src/client/contract.ts","../src/client/api.ts","../src/client/UpdaterPanel.tsx","../src/client/UpdaterDialog.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * client-ui-updater 词典(纯数据模块:零运行时依赖)。\n *\n * 单一来源:本包 client apply 注册 zh/en(typed register 编译期校验键位);\n * packages/dsh-i18n 语言包构建期 import 本模块注册 zh-CN;scripts/i18n-audit.mjs\n * 静态加载本模块做 zh/en 键对齐与占位符对齐门禁。\n */\nimport type { UpdaterLocaleKey } from './contract.ts'\n\nexport const zh: Record<UpdaterLocaleKey, string> = {\n nav: '软件更新',\n lead: '检测 GitHub Releases 上的新版本,增量更新只下载变动的交易插件包,无需重装应用。',\n currentVersion: '交易插件版本',\n desktopApp: '桌面应用版本',\n unsupported: '当前环境不支持自动增量更新(仅 DSH Trading 桌面版支持)。可以前往发布页手动下载完整安装包。',\n viewReleases: '在 GitHub 查看发布',\n checkNow: '检查更新',\n checking: '正在检查…',\n lastCheck: '上次检查',\n never: '尚未检查',\n upToDate: '已是最新版本。',\n available: '发现新版本 {version}',\n publishedAt: '发布于 {date}',\n notesTitle: '更新说明',\n applyNow: '立即更新',\n payloadMissing: '该版本未附带增量更新包,请前往发布页下载完整安装包。',\n 'phase.prepare': '正在准备…',\n 'phase.download': '正在下载增量包…',\n 'phase.verify': '正在校验增量包…',\n 'phase.install': '正在安装更新…',\n progress: '{percent}%',\n done: '已更新到 {version}',\n restartHint: '重启 DSH Trading 后生效。',\n restartNow: '重启应用',\n restartManual: '请手动重启 DSH Trading 以完成更新。',\n checkError: '检查失败:{message}',\n retry: '重试',\n close: '关闭',\n}\n\nexport const en: Record<UpdaterLocaleKey, string> = {\n nav: 'Software updates',\n lead: 'Detects new versions published on GitHub Releases and applies incremental updates — only the changed trading plugin packages are downloaded, no reinstall needed.',\n currentVersion: 'Trading plugins',\n desktopApp: 'Desktop app',\n unsupported: 'Incremental updates are only available inside the DSH Trading desktop app. You can download a full installer from the releases page.',\n viewReleases: 'View releases on GitHub',\n checkNow: 'Check for updates',\n checking: 'Checking…',\n lastCheck: 'Last checked',\n never: 'Never checked',\n upToDate: 'You are up to date.',\n available: 'New version {version} available',\n publishedAt: 'Published {date}',\n notesTitle: 'Release notes',\n applyNow: 'Update now',\n payloadMissing: 'This release does not carry the incremental update payload. Please download a full installer from the releases page.',\n 'phase.prepare': 'Preparing…',\n 'phase.download': 'Downloading update payload…',\n 'phase.verify': 'Verifying update payload…',\n 'phase.install': 'Installing update…',\n progress: '{percent}%',\n done: 'Updated to {version}',\n restartHint: 'Restart DSH Trading to activate.',\n restartNow: 'Restart app',\n restartManual: 'Please restart DSH Trading manually to finish the update.',\n checkError: 'Check failed: {message}',\n retry: 'Retry',\n close: 'Close',\n}\n","/**\n * Locale contract for the software-update dialog. The zh dictionary in\n * locales.ts is the key source of truth; this union + the LocaleNamespaceMap\n * merge give the framework-injected t seat its key types.\n *\n * Type-only imports pull the host slot augmentations (shell.overlay) into the\n * program so the dialog registers against a declared slot.\n */\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport type {} from '@deepseek-ai/dsh-client-ui-layout/client'\n\nexport type UpdaterLocaleKey =\n | 'nav'\n | 'lead'\n | 'currentVersion'\n | 'desktopApp'\n | 'unsupported'\n | 'viewReleases'\n | 'checkNow'\n | 'checking'\n | 'lastCheck'\n | 'never'\n | 'upToDate'\n | 'available'\n | 'publishedAt'\n | 'notesTitle'\n | 'applyNow'\n | 'payloadMissing'\n | 'phase.prepare'\n | 'phase.download'\n | 'phase.verify'\n | 'phase.install'\n | 'progress'\n | 'done'\n | 'restartHint'\n | 'restartNow'\n | 'restartManual'\n | 'checkError'\n | 'retry'\n | 'close'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Software-update dialog copy (check / notes / incremental apply). */\n 'dshtrading.updater': UpdaterLocaleKey\n }\n}\n\n/**\n * Window event contract with the trading shell sidebar (client-ui-trading\n * MarketDock update entry). Cross-plugin coupling is intentionally a DOM\n * custom event, not an import (client plugins never import each other's\n * modules).\n *\n * - UPDATE_AVAILABLE_EVENT: fired by the dialog whenever the host snapshot\n * changes. Detail: { available: boolean, version?: string }.\n * - UPDATER_OPEN_EVENT: fired by the sidebar update entry to ask this plugin\n * to open its dialog. No detail.\n */\nexport const UPDATE_AVAILABLE_EVENT = 'dshtrading-update-available'\nexport const UPDATER_OPEN_EVENT = 'dshtrading-updater-open'\n","/**\n * Updater bridge client: same-origin fetch wrappers over\n * /dshtrading/api/updater (the node half registers the route behind the\n * browser-auth fence; same-origin fetch carries the auth cookie by default).\n * Error semantics mirror the tasks bridge: non-2xx or { ok:false } envelopes\n * become rejections carrying the business code.\n */\nimport type { UpdaterSnapshot } from '../updater-service.ts'\n\nexport class UpdaterBridgeError extends Error {\n constructor(readonly status: number, message: string, readonly code?: string) {\n super(message)\n }\n}\n\nasync function updaterJson<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await fetch(path, init)\n if (!response.ok) {\n const body = await response.json().catch(() => undefined) as { code?: string; message?: string } | undefined\n const detail = typeof body?.message === 'string' && body.message !== '' ? ': ' + body.message : ''\n throw new UpdaterBridgeError(response.status, 'updater ' + path + ' failed: ' + String(response.status) + detail, body?.code)\n }\n const wire = await response.json() as T\n if (wire !== null && typeof wire === 'object' && (wire as { ok?: unknown }).ok === false) {\n const business = wire as { code?: string; message?: string }\n throw new UpdaterBridgeError(200, (business.code ?? 'UPDATER_UNKNOWN') + ': ' + (business.message ?? 'updater bridge error'), business.code)\n }\n return wire\n}\n\n/** Current updater snapshot (environment + check + apply state). */\nexport async function fetchUpdaterState(): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>('/dshtrading/api/updater/state', { headers: { accept: 'application/json' } })\n}\n\n/** Run a GitHub release check; force=true bypasses the host-side TTL cache. */\nexport async function requestUpdaterCheck(force: boolean): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>(`/dshtrading/api/updater/check?force=${force ? '1' : '0'}`, { headers: { accept: 'application/json' } })\n}\n\n/** Start the incremental apply pipeline; returns the running snapshot. */\nexport async function requestUpdaterApply(): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>('/dshtrading/api/updater/apply', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: '{}',\n })\n}\n\n/** Electron shell bridge exposed by the desktop preload (absent in browsers). */\ninterface DesktopBridge {\n relaunch?: () => void\n}\n\n/** Resolve the desktop shell bridge, if this page runs inside DSH Trading. */\nexport function desktopBridge(): DesktopBridge | undefined {\n if (typeof window === 'undefined' || !('desktop' in window)) return undefined\n return (window as { desktop?: DesktopBridge }).desktop\n}\n","/**\n * Software-update dialog body (rendered inside UpdaterDialog). Pure view over\n * /dshtrading/api/updater: renders the current versions, the latest GitHub\n * release with its notes, and drives the incremental apply + restart flow.\n * Polls the host snapshot while mounted (faster while an apply is running);\n * no SSE dependency.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { UpdaterSnapshot } from '../updater-service.ts'\nimport { desktopBridge, fetchUpdaterState, requestUpdaterApply, requestUpdaterCheck } from './api.ts'\nimport { UPDATE_AVAILABLE_EVENT } from './contract.ts'\nimport css from './updater-panel.module.css'\n\nexport type UpdaterPanelProps = PropsLocale<'dshtrading.updater'>\n\n/** Poll cadences: lazy when idle, tight while an apply pipeline is running. */\nconst POLL_IDLE_MS = 30_000\nconst POLL_RUNNING_MS = 1_200\n\n/** ISO date (yyyy-mm-dd) for the published line; empty when unknown. */\nfunction formatDay(iso: string | undefined): string {\n if (iso === undefined) return ''\n const parsed = new Date(iso)\n if (Number.isNaN(parsed.getTime())) return ''\n return parsed.toISOString().slice(0, 10)\n}\n\nexport function UpdaterPanel({ t }: UpdaterPanelProps) {\n const [state, setState] = useState<UpdaterSnapshot | undefined>(undefined)\n const [actionError, setActionError] = useState<string | undefined>(undefined)\n const [checkBusy, setCheckBusy] = useState(false)\n const applyBusyRef = useRef(false)\n\n const running = state?.apply.phase === 'running'\n\n const refresh = useCallback(async () => {\n try {\n setState(await fetchUpdaterState())\n } catch {\n // Bridge absent (headless profile / old deployment): keep the page quiet.\n }\n }, [])\n\n useEffect(() => { void refresh() }, [refresh])\n\n useEffect(() => {\n const interval = setInterval(() => { void refresh() }, running ? POLL_RUNNING_MS : POLL_IDLE_MS)\n return () => { clearInterval(interval) }\n }, [refresh, running])\n\n // Sidebar badge sync: mirror availability onto the window event the trading\n // shell sidebar listens to (see UPDATE_AVAILABLE_EVENT in contract.ts).\n useEffect(() => {\n if (state === undefined) return\n const available = state.check.available === true && state.apply.phase !== 'done'\n window.dispatchEvent(new CustomEvent(UPDATE_AVAILABLE_EVENT, {\n detail: { available, version: state.check.latest?.version },\n }))\n }, [state])\n\n const handleCheck = async (): Promise<void> => {\n setCheckBusy(true)\n setActionError(undefined)\n try {\n setState(await requestUpdaterCheck(true))\n } catch (error) {\n setActionError(error instanceof Error ? error.message : String(error))\n } finally {\n setCheckBusy(false)\n }\n }\n\n const handleApply = async (): Promise<void> => {\n if (applyBusyRef.current) return\n applyBusyRef.current = true\n setActionError(undefined)\n try {\n setState(await requestUpdaterApply())\n // Pipeline started: tighten polling immediately instead of waiting a tick.\n void refresh()\n } catch (error) {\n setActionError(error instanceof Error ? error.message : String(error))\n } finally {\n applyBusyRef.current = false\n }\n }\n\n const handleRestart = (): void => {\n desktopBridge()?.relaunch?.()\n }\n\n const environment = state?.environment\n const check = state?.check\n const apply = state?.apply\n const progress = state?.progress\n const latest = check?.latest\n const releasesUrl = latest?.url ?? 'https://github.com/zhu1090093659/dsh-trading/releases'\n const publishedDay = formatDay(latest?.publishedAt)\n const hasUpdate = check?.available === true && apply?.phase !== 'done'\n const applyable = hasUpdate && latest?.payloadAvailable === true\n\n return (\n <div className={css.root}>\n <p className={css.lead}>{t('lead')}</p>\n\n <div className={css.versions}>\n <div className={css.versionItem}>\n <span className={css.versionLabel}>{t('currentVersion')}</span>\n <span className={css.versionValue}>{environment?.familyVersion ?? '—'}</span>\n </div>\n {environment?.appVersion !== undefined && (\n <div className={css.versionItem}>\n <span className={css.versionLabel}>{t('desktopApp')}</span>\n <span className={css.versionValue}>{environment.appVersion}</span>\n </div>\n )}\n </div>\n\n {environment !== undefined && !environment.supported && (\n <p className={css.statusText}>{t('unsupported')}</p>\n )}\n\n {environment?.supported === true && (\n <>\n <div className={css.statusRow}>\n {check?.status === 'error' && (\n <span className={`${css.statusText} ${css.statusTextError}`}>\n {t('checkError', { message: check.error ?? '' })}\n </span>\n )}\n {check?.status === 'ok' && !hasUpdate && apply?.phase !== 'done' && (\n <span className={`${css.statusText} ${css.statusTextOk}`}>{t('upToDate')}</span>\n )}\n {check?.at !== undefined && (\n <span className={css.statusText}>\n {t('lastCheck')}\n :\n {' '}\n {check.at.slice(0, 19).replace('T', ' ')}\n </span>\n )}\n {check?.at === undefined && check?.status !== 'ok' && (\n <span className={css.statusText}>{t('never')}</span>\n )}\n <button\n type=\"button\"\n className={css.secondaryButton}\n disabled={checkBusy || running}\n onClick={() => { void handleCheck() }}\n >\n {checkBusy ? t('checking') : t('checkNow')}\n </button>\n {actionError !== undefined && (\n <button type=\"button\" className={css.secondaryButton} onClick={() => { void handleCheck() }}>\n {t('retry')}\n </button>\n )}\n </div>\n\n {running && (\n <div className={css.progressWrap}>\n <span className={css.progressLabel}>\n <span>\n {progress?.step === 'download'\n ? t('phase.download')\n : progress?.step === 'verify'\n ? t('phase.verify')\n : progress?.step === 'install'\n ? t('phase.install')\n : t('phase.prepare')}\n </span>\n {progress?.percent !== undefined && (\n <span>{t('progress', { percent: progress.percent })}</span>\n )}\n </span>\n <div\n className={css.progressTrack}\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={progress?.percent ?? undefined}\n >\n <div\n className={`${css.progressFill} ${progress?.percent === undefined ? css.progressIndeterminate : ''}`}\n style={progress?.percent === undefined ? undefined : { width: `${progress.percent}%` }}\n />\n </div>\n </div>\n )}\n\n {hasUpdate && latest !== undefined && !running && (\n <div className={css.availableCard}>\n <p className={css.availableTitle}>{t('available', { version: latest.version })}</p>\n {publishedDay !== '' && (\n <p className={css.availableMeta}>{t('publishedAt', { date: publishedDay })}</p>\n )}\n {latest.notes.trim() !== '' && (\n <>\n <span className={css.notesTitle}>{t('notesTitle')}</span>\n <pre className={css.notes}>{latest.notes}</pre>\n </>\n )}\n <div className={css.actions}>\n {latest.payloadAvailable && (\n <button\n type=\"button\"\n className={css.primaryButton}\n onClick={() => { void handleApply() }}\n >\n {t('applyNow')}\n </button>\n )}\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n </div>\n {!latest.payloadAvailable && (\n <p className={css.doneMeta}>{t('payloadMissing')}</p>\n )}\n </div>\n )}\n\n {apply?.phase === 'done' && apply.targetVersion !== undefined && (\n <div className={css.doneCard}>\n <p className={css.doneTitle}>{t('done', { version: apply.targetVersion })}</p>\n <p className={css.doneMeta}>{t('restartHint')}</p>\n <div className={css.actions}>\n {desktopBridge()?.relaunch !== undefined && (\n <button type=\"button\" className={css.primaryButton} onClick={handleRestart}>\n {t('restartNow')}\n </button>\n )}\n {desktopBridge()?.relaunch === undefined && (\n <span className={css.doneMeta}>{t('restartManual')}</span>\n )}\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n </div>\n </div>\n )}\n\n {apply?.phase === 'error' && apply.error !== undefined && (\n <p className={css.errorText}>{apply.error}</p>\n )}\n </>\n )}\n\n {environment === undefined || !environment.supported ? (\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n ) : null}\n </div>\n )\n}\n","/**\n * Software-update dialog (the 'shell.overlay' entry registered by this plugin).\n * The sidebar's update entry dispatches UPDATER_OPEN_EVENT; this component owns\n * the modal chrome (mask click + Escape close) around UpdaterPanel. Frame-wide\n * floating layer: fixed overlay opts back into pointer events.\n */\nimport { useCallback, useEffect, useState } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport { UPDATER_OPEN_EVENT } from './contract.ts'\nimport { UpdaterPanel } from './UpdaterPanel.tsx'\nimport css from './updater-dialog.module.css'\n\nexport type UpdaterDialogProps = PropsLocale<'dshtrading.updater'>\n\nexport function UpdaterDialog({ t }: UpdaterDialogProps) {\n const [open, setOpen] = useState(false)\n\n useEffect(() => {\n const onOpen = (): void => { setOpen(true) }\n window.addEventListener(UPDATER_OPEN_EVENT, onOpen)\n return () => { window.removeEventListener(UPDATER_OPEN_EVENT, onOpen) }\n }, [])\n\n const close = useCallback((): void => { setOpen(false) }, [])\n\n useEffect(() => {\n if (!open) return\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') close()\n }\n document.addEventListener('keydown', onKeyDown)\n return () => { document.removeEventListener('keydown', onKeyDown) }\n }, [open, close])\n\n if (!open) return null\n\n return (\n <div className={css.overlay} role=\"presentation\">\n <div className={css.mask} aria-hidden=\"true\" onClick={close} />\n <div className={css.panel} role=\"dialog\" aria-modal=\"true\" aria-label={t('nav')}>\n <div className={css.header}>\n <span className={css.title}>{t('nav')}</span>\n <button type=\"button\" className={css.close} aria-label={t('close')} title={t('close')} onClick={close}>\n <svg viewBox=\"0 0 16 16\" width={14} height={14} aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M4.25 4.25l7.5 7.5M11.75 4.25l-7.5 7.5\" />\n </svg>\n </button>\n </div>\n <div className={css.body}>\n <UpdaterPanel t={t} />\n </div>\n </div>\n </div>\n )\n}\n","/**\n * dsh-trading auto-update, browser half — the software-update dialog on the\n * host's frame-wide shell.overlay layer. The entry point is the update button\n * beside Settings in the trading sidebar's bottom bar (client-ui-trading\n * dispatches UPDATER_OPEN_EVENT); this plugin owns the modal chrome and body.\n *\n * The dialog talks to the node half over /dshtrading/api/updater (same-origin\n * fetch inside the browser-auth fence). In environments without incremental\n * support (dev checkouts, headless profiles) it degrades to an information\n * surface linking the GitHub releases page.\n */\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from './contract.ts'\nimport { en, zh } from './locales.ts'\nimport { UpdaterDialog } from './UpdaterDialog.tsx'\n\nimport { UPDATE_AVAILABLE_EVENT, UPDATER_OPEN_EVENT } from './contract.ts'\n/** This dialog's locale namespace. */\nconst NS = 'dshtrading.updater'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale']\n\n/**\n * Register the update dialog on shell.overlay (no settings.section: the entry\n * moved beside Settings in the sidebar bottom bar). The sidebar asks this\n * plugin to open through the UPDATER_OPEN_EVENT window event — cross-plugin\n * coupling is a DOM event, never an import.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.locale.bind(NS)\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-trading-updater: dictionaries')\n\n ctx.slots.inject('shell.overlay', () => ctx.slots.register({\n name: 'shell.overlay',\n id: 'dshtrading-updater-dialog',\n order: 90,\n locale: NS,\n }, UpdaterDialog))\n}\n\nexport { UPDATE_AVAILABLE_EVENT, UPDATER_OPEN_EVENT } from './contract.ts'\n"],"mappings":"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"}
|
package/lib/github.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
//#region src/github.ts
|
|
2
|
+
/**
|
|
3
|
+
* GitHub Releases client for the update channel. Detection uses the public
|
|
4
|
+
* REST API (repos/<owner>/<repo>/releases/latest) which already excludes
|
|
5
|
+
* drafts and prereleases — the release pipeline only publishes stable tags.
|
|
6
|
+
* No token: the repo is public and the cadence (startup + 6h) sits far below
|
|
7
|
+
* the unauthenticated rate limit.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* GitHub refused the request because this address is over its request budget.
|
|
11
|
+
* The unauthenticated core limit answers 403 with `x-ratelimit-remaining: 0`
|
|
12
|
+
* (measured 2026-10-15 on this deployment: 60/60 used, reset header present),
|
|
13
|
+
* and the secondary limit answers 429 with `retry-after`. Carries the reset
|
|
14
|
+
* instant so the caller can back off instead of retrying into the same wall.
|
|
15
|
+
*/
|
|
16
|
+
var GithubRateLimitError = class extends Error {
|
|
17
|
+
/** Epoch ms when the budget resets; undefined when GitHub reported no hint. */
|
|
18
|
+
resetAt;
|
|
19
|
+
constructor(repo, status, resetAt) {
|
|
20
|
+
super(`GitHub API ${status} rate limit for ${repo} releases/latest`);
|
|
21
|
+
this.name = "GithubRateLimitError";
|
|
22
|
+
this.resetAt = resetAt;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
2
25
|
const GITHUB_API_BASE = "https://api.github.com";
|
|
3
26
|
/** Update payload manifest asset name for a tag, e.g. updates-manifest-v0.2.0.json. */
|
|
4
27
|
function manifestAssetName(tagName) {
|
|
@@ -19,6 +42,23 @@ function parseAsset(raw) {
|
|
|
19
42
|
};
|
|
20
43
|
}
|
|
21
44
|
/**
|
|
45
|
+
* When the next request may succeed again, from the headers GitHub sends with a
|
|
46
|
+
* refused-for-budget response: `retry-after` (seconds, secondary limit) wins
|
|
47
|
+
* over the absolute `x-ratelimit-reset` (epoch seconds, primary limit).
|
|
48
|
+
*/
|
|
49
|
+
function rateLimitResetAt(response) {
|
|
50
|
+
const retryAfter = response.headers.get("retry-after");
|
|
51
|
+
if (retryAfter !== null) {
|
|
52
|
+
const seconds = Number(retryAfter);
|
|
53
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Date.now() + seconds * 1e3;
|
|
54
|
+
}
|
|
55
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
56
|
+
if (reset !== null) {
|
|
57
|
+
const seconds = Number(reset);
|
|
58
|
+
if (Number.isFinite(seconds) && seconds > 0) return seconds * 1e3;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
22
62
|
* Fetch the latest published release. Returns undefined when the repo has no
|
|
23
63
|
* full releases yet (GitHub answers 404 for /releases/latest then).
|
|
24
64
|
*/
|
|
@@ -39,7 +79,10 @@ async function fetchLatestRelease(options) {
|
|
|
39
79
|
signal: controller.signal
|
|
40
80
|
});
|
|
41
81
|
if (response.status === 404) return void 0;
|
|
42
|
-
if (!response.ok)
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
if (response.status === 429 || response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0") throw new GithubRateLimitError(options.repo, response.status, rateLimitResetAt(response));
|
|
84
|
+
throw new Error(`GitHub API ${response.status} for ${options.repo} releases/latest`);
|
|
85
|
+
}
|
|
43
86
|
const body = await response.json();
|
|
44
87
|
const tagName = typeof body.tag_name === "string" ? body.tag_name : "";
|
|
45
88
|
const assets = Array.isArray(body.assets) ? body.assets.map(parseAsset).filter((asset) => asset !== void 0) : [];
|
|
@@ -82,4 +125,4 @@ function parseUpdateManifest(text) {
|
|
|
82
125
|
};
|
|
83
126
|
}
|
|
84
127
|
//#endregion
|
|
85
|
-
export { GITHUB_API_BASE, fetchLatestRelease, manifestAssetName, parseUpdateManifest, payloadAssetName };
|
|
128
|
+
export { GITHUB_API_BASE, GithubRateLimitError, fetchLatestRelease, manifestAssetName, parseUpdateManifest, payloadAssetName };
|
package/lib/updater-service.d.ts
CHANGED
|
@@ -86,6 +86,8 @@ declare class UpdaterService {
|
|
|
86
86
|
private readonly checkTtlMs;
|
|
87
87
|
private readonly log;
|
|
88
88
|
private checkState;
|
|
89
|
+
/** Epoch ms before which a non-forced check must not ask GitHub again. */
|
|
90
|
+
private rateLimitedUntil;
|
|
89
91
|
private applyState;
|
|
90
92
|
private progress;
|
|
91
93
|
private timers;
|
|
@@ -101,6 +103,11 @@ declare class UpdaterService {
|
|
|
101
103
|
* and double as the periodic tick; force=true bypasses the cache (UI button).
|
|
102
104
|
*/
|
|
103
105
|
check(force: boolean): Promise<UpdaterSnapshot>;
|
|
106
|
+
/**
|
|
107
|
+
* Whether a non-forced check can reuse current state: a fresh `ok` result, or
|
|
108
|
+
* an active rate-limit cooldown (a known-refused budget must not be re-probed
|
|
109
|
+
* on every launch).
|
|
110
|
+
*/
|
|
104
111
|
private checkFresh;
|
|
105
112
|
private latestOf;
|
|
106
113
|
/**
|
package/lib/updater-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchLatestRelease, manifestAssetName, parseUpdateManifest, payloadAssetName } from "./github.js";
|
|
1
|
+
import { GithubRateLimitError, fetchLatestRelease, manifestAssetName, parseUpdateManifest, payloadAssetName } from "./github.js";
|
|
2
2
|
import { isNewerVersion } from "./semver.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs";
|
|
@@ -33,6 +33,13 @@ var UpdaterError = class extends Error {
|
|
|
33
33
|
const AUTO_CHECK_MS_DEFAULT = 216e5;
|
|
34
34
|
const INITIAL_DELAY_MS_DEFAULT = 15e3;
|
|
35
35
|
const CHECK_TTL_MS_DEFAULT = 18e5;
|
|
36
|
+
/**
|
|
37
|
+
* Quiet period after GitHub answers "over budget" without a reset hint. The
|
|
38
|
+
* unauthenticated budget is per address and refills hourly, and every launch of
|
|
39
|
+
* the app runs one check — retrying immediately turns one exhausted budget into
|
|
40
|
+
* a failed-looking log line per launch (measured 2026-10-15).
|
|
41
|
+
*/
|
|
42
|
+
const RATE_LIMIT_COOLDOWN_MS_DEFAULT = 9e5;
|
|
36
43
|
/** Windows rename retries for files/dirs briefly held open (AV scans etc.). */
|
|
37
44
|
const SWAP_RETRY_ATTEMPTS = 4;
|
|
38
45
|
const SWAP_RETRY_DELAY_MS = 300;
|
|
@@ -90,6 +97,8 @@ var UpdaterService = class {
|
|
|
90
97
|
checkTtlMs;
|
|
91
98
|
log;
|
|
92
99
|
checkState;
|
|
100
|
+
/** Epoch ms before which a non-forced check must not ask GitHub again. */
|
|
101
|
+
rateLimitedUntil = 0;
|
|
93
102
|
applyState = applyIdle();
|
|
94
103
|
progress = void 0;
|
|
95
104
|
timers = [];
|
|
@@ -147,6 +156,7 @@ var UpdaterService = class {
|
|
|
147
156
|
});
|
|
148
157
|
const latest = release === void 0 ? void 0 : this.latestOf(release);
|
|
149
158
|
const available = latest?.version !== void 0 && isNewerVersion(latest.version, this.env.familyVersion);
|
|
159
|
+
this.rateLimitedUntil = 0;
|
|
150
160
|
this.checkState = {
|
|
151
161
|
at: new Date(this.now()).toISOString(),
|
|
152
162
|
status: "ok",
|
|
@@ -156,21 +166,33 @@ var UpdaterService = class {
|
|
|
156
166
|
};
|
|
157
167
|
this.persistCheck();
|
|
158
168
|
} catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
if (error instanceof GithubRateLimitError) {
|
|
171
|
+
const until = error.resetAt ?? this.now() + RATE_LIMIT_COOLDOWN_MS_DEFAULT;
|
|
172
|
+
this.rateLimitedUntil = Math.max(this.rateLimitedUntil, until);
|
|
173
|
+
}
|
|
159
174
|
this.checkState = {
|
|
160
175
|
at: new Date(this.now()).toISOString(),
|
|
161
176
|
status: "error",
|
|
162
|
-
error:
|
|
177
|
+
error: message,
|
|
163
178
|
available: this.checkState.available,
|
|
164
179
|
latest: this.checkState.latest
|
|
165
180
|
};
|
|
166
181
|
this.persistCheck();
|
|
167
|
-
this.log("check
|
|
182
|
+
if (error instanceof GithubRateLimitError) this.log("check rate limited: " + message + ", next attempt after " + new Date(this.rateLimitedUntil).toISOString());
|
|
183
|
+
else this.log("check failed: " + message);
|
|
168
184
|
} finally {
|
|
169
185
|
this.checkInFlight = false;
|
|
170
186
|
}
|
|
171
187
|
return this.snapshot();
|
|
172
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Whether a non-forced check can reuse current state: a fresh `ok` result, or
|
|
191
|
+
* an active rate-limit cooldown (a known-refused budget must not be re-probed
|
|
192
|
+
* on every launch).
|
|
193
|
+
*/
|
|
173
194
|
checkFresh() {
|
|
195
|
+
if (this.now() < this.rateLimitedUntil) return true;
|
|
174
196
|
if (this.checkState.status !== "ok" || this.checkState.at === void 0) return false;
|
|
175
197
|
const at = Date.parse(this.checkState.at);
|
|
176
198
|
if (!Number.isFinite(at)) return false;
|
|
@@ -460,7 +482,9 @@ var UpdaterService = class {
|
|
|
460
482
|
}
|
|
461
483
|
loadPersistedCheck() {
|
|
462
484
|
try {
|
|
463
|
-
const
|
|
485
|
+
const raw = JSON.parse(fs.readFileSync(this.stateFile().file, "utf8"));
|
|
486
|
+
if (typeof raw.rateLimitedUntil === "number" && Number.isFinite(raw.rateLimitedUntil) && raw.rateLimitedUntil > 0) this.rateLimitedUntil = raw.rateLimitedUntil;
|
|
487
|
+
const lastCheck = raw.lastCheck;
|
|
464
488
|
if (lastCheck !== void 0 && typeof lastCheck === "object" && "status" in lastCheck) return {
|
|
465
489
|
at: lastCheck.at,
|
|
466
490
|
status: lastCheck.status,
|
|
@@ -483,7 +507,8 @@ var UpdaterService = class {
|
|
|
483
507
|
fs.mkdirSync(dir, { recursive: true });
|
|
484
508
|
const state = {
|
|
485
509
|
version: 1,
|
|
486
|
-
lastCheck: this.checkState
|
|
510
|
+
lastCheck: this.checkState,
|
|
511
|
+
...this.rateLimitedUntil > 0 ? { rateLimitedUntil: this.rateLimitedUntil } : {}
|
|
487
512
|
};
|
|
488
513
|
const tmp = file + ".tmp";
|
|
489
514
|
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/client-ui-updater",
|
|
3
3
|
"description": "dsh-trading auto-update plugin: GitHub Releases version check with release notes, incremental update of the @dshtrading/* plugin family inside a desktop-seeded profile (vendor payload zip, per-package swap), and the software-update dialog opened from the trading sidebar's update entry. Browser half registers the shell.overlay dialog; node half registers the /dshtrading/api/updater routes and the update engine.",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"default": "./lib/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./client": {
|
|
14
|
-
"types": "./lib/types/client/index.d.ts",
|
|
15
14
|
"default": "./lib/client.js"
|
|
16
15
|
},
|
|
17
16
|
"./locales": {
|
|
@@ -26,13 +25,13 @@
|
|
|
26
25
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
27
26
|
"dependencies": {
|
|
28
27
|
"fflate": "^0.8.2",
|
|
29
|
-
"@dshtrading/dsh-home": "^0.6.
|
|
28
|
+
"@dshtrading/dsh-home": "^0.6.3"
|
|
30
29
|
},
|
|
31
30
|
"peerDependencies": {
|
|
32
31
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
33
|
-
"@deepseek-ai/dsh-client-locale": ">=0.2.
|
|
34
|
-
"@deepseek-ai/dsh-client-ui-layout": ">=0.2.
|
|
35
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.
|
|
32
|
+
"@deepseek-ai/dsh-client-locale": ">=0.2.1-rc.2",
|
|
33
|
+
"@deepseek-ai/dsh-client-ui-layout": ">=0.2.1-rc.2",
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.2.1-rc.2",
|
|
36
35
|
"react": "^18.2.0",
|
|
37
36
|
"@types/react": "~18.3.1"
|
|
38
37
|
},
|
|
@@ -46,10 +45,10 @@
|
|
|
46
45
|
}
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
50
|
-
"@deepseek-ai/dsh-client-locale": "0.2.
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-layout": "0.2.
|
|
52
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.
|
|
48
|
+
"@deepseek-ai/cordis": "^4.0.5",
|
|
49
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-layout": "0.2.1-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
|
|
53
52
|
"@types/node": "^24.0.0",
|
|
54
53
|
"@types/react": "~18.3.1",
|
|
55
54
|
"lightningcss": "^1.0.0",
|