dsh-zen-remote 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -0
- package/LICENSE +38 -0
- package/README.md +237 -0
- package/assets/home.png +0 -0
- package/assets/info.png +0 -0
- package/assets/pairing.png +0 -0
- package/assets/session.png +0 -0
- package/assets/sheet.png +0 -0
- package/cordis.patch.yml +17 -0
- package/cordis.patch.yml.example +16 -0
- package/docs/interface.md +200 -0
- package/docs/remote-access.en.md +335 -0
- package/docs/remote-access.md +422 -0
- package/dsh-push.mjs +213 -0
- package/lan-gate.mjs +78 -0
- package/lib/client.js +6615 -0
- package/lib/index.js +310 -0
- package/lib/index.js.map +1 -0
- package/lib/lan-gate-server.cjs +626 -0
- package/lib/types/client/MobileAttachButton.d.ts +25 -0
- package/lib/types/client/MobileAttachButton.d.ts.map +1 -0
- package/lib/types/client/MobileAttachChips.d.ts +32 -0
- package/lib/types/client/MobileAttachChips.d.ts.map +1 -0
- package/lib/types/client/MobileDrawerFooter.d.ts +21 -0
- package/lib/types/client/MobileDrawerFooter.d.ts.map +1 -0
- package/lib/types/client/MobileHome.d.ts +24 -0
- package/lib/types/client/MobileHome.d.ts.map +1 -0
- package/lib/types/client/MobileHomeChips.d.ts +76 -0
- package/lib/types/client/MobileHomeChips.d.ts.map +1 -0
- package/lib/types/client/MobileNavOverlay.d.ts +15 -0
- package/lib/types/client/MobileNavOverlay.d.ts.map +1 -0
- package/lib/types/client/MobileNavToggle.d.ts +18 -0
- package/lib/types/client/MobileNavToggle.d.ts.map +1 -0
- package/lib/types/client/MobileSessionHeader.d.ts +58 -0
- package/lib/types/client/MobileSessionHeader.d.ts.map +1 -0
- package/lib/types/client/MobileSessionInfo.d.ts +41 -0
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -0
- package/lib/types/client/attach-upload.d.ts +78 -0
- package/lib/types/client/attach-upload.d.ts.map +1 -0
- package/lib/types/client/chips-store.d.ts +13 -0
- package/lib/types/client/chips-store.d.ts.map +1 -0
- package/lib/types/client/debug.d.ts +9 -0
- package/lib/types/client/debug.d.ts.map +1 -0
- package/lib/types/client/effects/aionui-compat.d.ts +4 -0
- package/lib/types/client/effects/aionui-compat.d.ts.map +1 -0
- package/lib/types/client/effects/gestures.d.ts +7 -0
- package/lib/types/client/effects/gestures.d.ts.map +1 -0
- package/lib/types/client/effects/header-status.d.ts +15 -0
- package/lib/types/client/effects/header-status.d.ts.map +1 -0
- package/lib/types/client/effects/phone-chrome.d.ts +106 -0
- package/lib/types/client/effects/phone-chrome.d.ts.map +1 -0
- package/lib/types/client/effects/turn-fold.d.ts +25 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +18 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +58 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/nav-store.d.ts +73 -0
- package/lib/types/client/nav-store.d.ts.map +1 -0
- package/lib/types/client/session-dot.d.ts +12 -0
- package/lib/types/client/session-dot.d.ts.map +1 -0
- package/lib/types/client/styles/base.css.d.ts +2 -0
- package/lib/types/client/styles/base.css.d.ts.map +1 -0
- package/lib/types/client/styles/chips.css.d.ts +2 -0
- package/lib/types/client/styles/chips.css.d.ts.map +1 -0
- package/lib/types/client/styles/compat.css.d.ts +2 -0
- package/lib/types/client/styles/compat.css.d.ts.map +1 -0
- package/lib/types/client/styles/composer.css.d.ts +2 -0
- package/lib/types/client/styles/composer.css.d.ts.map +1 -0
- package/lib/types/client/styles/header.css.d.ts +2 -0
- package/lib/types/client/styles/header.css.d.ts.map +1 -0
- package/lib/types/client/styles/home.css.d.ts +2 -0
- package/lib/types/client/styles/home.css.d.ts.map +1 -0
- package/lib/types/client/styles/index.d.ts +15 -0
- package/lib/types/client/styles/index.d.ts.map +1 -0
- package/lib/types/client/styles/info.css.d.ts +2 -0
- package/lib/types/client/styles/info.css.d.ts.map +1 -0
- package/lib/types/client/styles/layout.css.d.ts +2 -0
- package/lib/types/client/styles/layout.css.d.ts.map +1 -0
- package/lib/types/client/styles/misc.css.d.ts +2 -0
- package/lib/types/client/styles/misc.css.d.ts.map +1 -0
- package/lib/types/client/styles/turn-fold.css.d.ts +2 -0
- package/lib/types/client/styles/turn-fold.css.d.ts.map +1 -0
- package/lib/types/index.d.ts +83 -0
- package/lib/types/index.d.ts.map +1 -0
- package/llms.txt +20 -0
- package/package.json +128 -0
- package/pwa/app.css +95 -0
- package/pwa/icons/icon-192.png +0 -0
- package/pwa/icons/icon-512.png +0 -0
- package/pwa/icons/icon-maskable-512.png +0 -0
- package/pwa/icons/icon-maskable.svg +1 -0
- package/pwa/icons/icon.svg +1 -0
- package/pwa/inject.js +190 -0
- package/pwa/manifest.json +29 -0
- package/pwa/offline.html +38 -0
- package/pwa/sw.js +176 -0
- package/pwa/touch-gestures.js +94 -0
- package/scripts/build-client.mjs +101 -0
- package/scripts/cdp-probe.mjs +64 -0
- package/scripts/check-attach-upload.mjs +99 -0
- package/scripts/check-sunk-viewport.mjs +33 -0
- package/scripts/check-upload-endpoint.mjs +168 -0
- package/src/client/MobileAttachButton.tsx +125 -0
- package/src/client/MobileAttachChips.tsx +85 -0
- package/src/client/MobileDrawerFooter.tsx +56 -0
- package/src/client/MobileHome.tsx +348 -0
- package/src/client/MobileHomeChips.tsx +426 -0
- package/src/client/MobileNavOverlay.tsx +279 -0
- package/src/client/MobileNavToggle.tsx +52 -0
- package/src/client/MobileSessionHeader.tsx +242 -0
- package/src/client/MobileSessionInfo.tsx +303 -0
- package/src/client/attach-upload.ts +140 -0
- package/src/client/chips-store.ts +41 -0
- package/src/client/debug.ts +253 -0
- package/src/client/effects/aionui-compat.ts +183 -0
- package/src/client/effects/gestures.ts +265 -0
- package/src/client/effects/header-status.ts +30 -0
- package/src/client/effects/phone-chrome.ts +265 -0
- package/src/client/effects/turn-fold.ts +258 -0
- package/src/client/index.tsx +227 -0
- package/src/client/locales.ts +109 -0
- package/src/client/nav-store.ts +78 -0
- package/src/client/session-dot.ts +17 -0
- package/src/client/styles/base.css.ts +167 -0
- package/src/client/styles/chips.css.ts +224 -0
- package/src/client/styles/compat.css.ts +681 -0
- package/src/client/styles/composer.css.ts +504 -0
- package/src/client/styles/header.css.ts +368 -0
- package/src/client/styles/home.css.ts +477 -0
- package/src/client/styles/index.ts +25 -0
- package/src/client/styles/info.css.ts +238 -0
- package/src/client/styles/layout.css.ts +501 -0
- package/src/client/styles/misc.css.ts +128 -0
- package/src/client/styles/turn-fold.css.ts +106 -0
- package/src/index.ts +343 -0
package/pwa/inject.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/* dsh-mobile-pwa · PWA bootstrap (injected into DSH page)
|
|
2
|
+
* Runs on every device (desktop included): registers the service worker
|
|
3
|
+
* (offline cache + notifications) and wires the agent-done push opt-in
|
|
4
|
+
* unconditionally. Touch gestures are the only piece gated by device kind —
|
|
5
|
+
* loaded on everything that isn't explicitly marked "desktop", see below.
|
|
6
|
+
*/
|
|
7
|
+
(function () {
|
|
8
|
+
'use strict'
|
|
9
|
+
if (!window.__DSH_PWA__) window.__DSH_PWA__ = {}
|
|
10
|
+
|
|
11
|
+
// ---- Register service worker ----------------------------------------
|
|
12
|
+
// Explicit scope: '/' — the script lives at /pwa/sw.js, so its default
|
|
13
|
+
// scope is only /pwa/ and it would never control the app itself (start_url
|
|
14
|
+
// "/"). The gateway sends Service-Worker-Allowed: / with the sw.js
|
|
15
|
+
// response so Chrome permits a scope wider than the script's own directory.
|
|
16
|
+
if ('serviceWorker' in navigator) {
|
|
17
|
+
window.addEventListener('load', () => {
|
|
18
|
+
navigator.serviceWorker.register('/pwa/sw.js', { scope: '/' }).then((reg) => {
|
|
19
|
+
window.__DSH_PWA__.reg = reg
|
|
20
|
+
}).catch((err) => {
|
|
21
|
+
console.warn('[dsh-pwa] SW registration failed:', err)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---- Load touch gestures on every non-desktop device -----------------
|
|
27
|
+
// A paired device defaults to kind "auto" — the gateway never sets
|
|
28
|
+
// data-lan-device for it (see lib/lan-gate-server.cjs), only for an
|
|
29
|
+
// explicit "phone" or "desktop". Gating on the literal "phone" value meant
|
|
30
|
+
// this never loaded on a real phone left at its default kind. Matching
|
|
31
|
+
// app.css's own `:not([data-lan-device="desktop"])` gate here instead:
|
|
32
|
+
// anything that isn't explicitly desktop gets touch gestures.
|
|
33
|
+
var isNonDesktop = document.documentElement.getAttribute('data-lan-device') !== 'desktop'
|
|
34
|
+
if (isNonDesktop) {
|
|
35
|
+
var g = document.createElement('script')
|
|
36
|
+
g.src = '/pwa/touch-gestures.js'
|
|
37
|
+
g.async = true
|
|
38
|
+
document.head.appendChild(g)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ---- Agent-done notification (Web Push) -----------------------------
|
|
42
|
+
// Subscribes to a gateway push endpoint. The gateway polls DSH event state and
|
|
43
|
+
// triggers push. Grant & subscribe are opt-in via the button appended below.
|
|
44
|
+
window.__DSH_PWA__.subscribe = function subscribe() {
|
|
45
|
+
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
46
|
+
alert('当前浏览器不支持推送通知')
|
|
47
|
+
return Promise.reject(new Error('push unsupported'))
|
|
48
|
+
}
|
|
49
|
+
return new Promise(function (resolve, reject) {
|
|
50
|
+
if (!window.__DSH_PWA__.reg) {
|
|
51
|
+
navigator.serviceWorker.ready.then(function (reg) {
|
|
52
|
+
window.__DSH_PWA__.reg = reg
|
|
53
|
+
doSubscribe(reg).then(resolve).catch(reject)
|
|
54
|
+
}).catch(reject)
|
|
55
|
+
} else {
|
|
56
|
+
doSubscribe(window.__DSH_PWA__.reg).then(resolve).catch(reject)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// VAPID public key arrives base64url-encoded via the injected bootstrap.
|
|
62
|
+
function vapidKeyBytes() {
|
|
63
|
+
var s = String(window.__DSH_PWA__.vapid || '')
|
|
64
|
+
if (!s) return undefined
|
|
65
|
+
s = s.replace(/-/g, '+').replace(/_/g, '/')
|
|
66
|
+
while (s.length % 4) s += '='
|
|
67
|
+
var raw = atob(s)
|
|
68
|
+
var a = new Uint8Array(raw.length)
|
|
69
|
+
for (var i = 0; i < raw.length; i++) a[i] = raw.charCodeAt(i)
|
|
70
|
+
return a
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function doSubscribe(reg) {
|
|
74
|
+
return reg.pushManager.subscribe({
|
|
75
|
+
userVisibleOnly: true,
|
|
76
|
+
applicationServerKey: vapidKeyBytes()
|
|
77
|
+
}).then(function (sub) {
|
|
78
|
+
// Notify the gateway so it can route push to this subscription.
|
|
79
|
+
return fetch('/pwa/push/subscribe', {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: { 'Content-Type': 'application/json' },
|
|
82
|
+
body: JSON.stringify({ subscription: sub.toJSON() })
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- Push opt-in ------------------------------------------------------
|
|
88
|
+
// Deliberately NOT gated on device kind: a device left at the default
|
|
89
|
+
// "auto" carries no data-lan-device attribute, and desktop browsers do
|
|
90
|
+
// Web Push too. Gating this on isPhone silently disabled push for every
|
|
91
|
+
// device that was never explicitly marked as a phone.
|
|
92
|
+
var pushSupported = 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window
|
|
93
|
+
var SNOOZE_KEY = 'dsh-pwa-notif-snooze'
|
|
94
|
+
var SNOOZE_MS = 7 * 24 * 3600 * 1000
|
|
95
|
+
// iOS grants Web Push only to home-screen installs, never to a Safari tab.
|
|
96
|
+
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
|
97
|
+
var isStandalone = window.navigator.standalone === true ||
|
|
98
|
+
(window.matchMedia && window.matchMedia('(display-mode: standalone)').matches)
|
|
99
|
+
|
|
100
|
+
function postSubscription(sub) {
|
|
101
|
+
return fetch('/pwa/push/subscribe', {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ subscription: sub.toJSON() })
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Permission already granted: make sure the gateway actually holds this
|
|
109
|
+
// device's subscription (it may have been lost to a state reset, or the
|
|
110
|
+
// original POST may have failed). No prompt is shown — none is needed.
|
|
111
|
+
function resyncPush() {
|
|
112
|
+
if (!pushSupported || window.Notification.permission !== 'granted') return
|
|
113
|
+
navigator.serviceWorker.ready.then(function (reg) {
|
|
114
|
+
return reg.pushManager.getSubscription().then(function (sub) {
|
|
115
|
+
if (sub) return postSubscription(sub)
|
|
116
|
+
return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidKeyBytes() }).then(postSubscription)
|
|
117
|
+
})
|
|
118
|
+
}).catch(function (err) { console.warn('[dsh-pwa] push resync failed:', err) })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function card(title, bodyHtml, buttonsHtml) {
|
|
122
|
+
var el = document.createElement('div')
|
|
123
|
+
el.id = 'dsh-pwa-notif-hint'
|
|
124
|
+
el.style.cssText =
|
|
125
|
+
'position:fixed;left:max(12px,env(safe-area-inset-left));right:max(12px,env(safe-area-inset-right));' +
|
|
126
|
+
'bottom:max(96px,calc(env(safe-area-inset-bottom) + 96px));z-index:2147483005;max-width:420px;margin:0 auto;' +
|
|
127
|
+
'background:rgba(22,26,34,.96);border:1px solid #2a2f3a;border-radius:14px;' +
|
|
128
|
+
'padding:14px 16px;color:#e6e8ec;font:13px/1.6 system-ui;' +
|
|
129
|
+
'-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);box-shadow:0 10px 30px rgba(0,0,0,.5)'
|
|
130
|
+
el.innerHTML = '<div style="font-weight:600;margin-bottom:4px">' + title + '</div>' +
|
|
131
|
+
'<div style="color:#9aa3b2;margin-bottom:10px">' + bodyHtml + '</div>' +
|
|
132
|
+
'<div style="display:flex;gap:8px">' + buttonsHtml + '</div>'
|
|
133
|
+
document.body.appendChild(el)
|
|
134
|
+
return el
|
|
135
|
+
}
|
|
136
|
+
var BTN_ON = '<button data-act="on" style="flex:1;background:#4c8dff;color:#fff;border:0;border-radius:9px;padding:9px 0;font-weight:600">开启</button>'
|
|
137
|
+
var BTN_OFF = '<button data-act="off" style="flex:1;background:#2a2f3a;color:#9aa3b2;border:0;border-radius:9px;padding:9px 0">暂不</button>'
|
|
138
|
+
var BTN_CLOSE = '<button data-act="off" style="flex:1;background:#2a2f3a;color:#9aa3b2;border:0;border-radius:9px;padding:9px 0">知道了</button>'
|
|
139
|
+
|
|
140
|
+
// Callable by hand at any time: window.__DSH_PWA__.askPush()
|
|
141
|
+
window.__DSH_PWA__.askPush = function askPush(manual) {
|
|
142
|
+
var old = document.getElementById('dsh-pwa-notif-hint')
|
|
143
|
+
if (old) old.remove()
|
|
144
|
+
if (!pushSupported) {
|
|
145
|
+
if (isIOS && !isStandalone) {
|
|
146
|
+
card('🔔 任务完成提醒', '在 iPhone 上,推送通知只对「添加到主屏幕」后的应用生效。请先用 Safari 的分享菜单添加到主屏幕,再从主屏图标打开本页。', BTN_CLOSE)
|
|
147
|
+
.querySelector('[data-act="off"]').addEventListener('click', function () { document.getElementById('dsh-pwa-notif-hint').remove() })
|
|
148
|
+
} else if (manual) {
|
|
149
|
+
alert('当前浏览器不支持 Web Push 通知')
|
|
150
|
+
}
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
if (window.Notification.permission === 'denied') {
|
|
154
|
+
if (manual) alert('通知权限已被拒绝,请到浏览器/系统的站点设置里重新允许')
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
if (window.Notification.permission === 'granted') { resyncPush(); return }
|
|
158
|
+
|
|
159
|
+
var el = card('🔔 任务完成提醒',
|
|
160
|
+
'开启后,智能体干完活会推送通知到这台设备,即使你切到别的 App。通知不含对话内容。',
|
|
161
|
+
BTN_ON + BTN_OFF)
|
|
162
|
+
el.querySelector('[data-act="on"]').addEventListener('click', function () {
|
|
163
|
+
el.remove()
|
|
164
|
+
// Must run inside the click gesture: Safari rejects subscribe() otherwise.
|
|
165
|
+
window.__DSH_PWA__.subscribe().catch(function (err) {
|
|
166
|
+
console.warn('[dsh-pwa] subscribe failed:', err)
|
|
167
|
+
alert('开启失败:' + (err && err.message ? err.message : err))
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
el.querySelector('[data-act="off"]').addEventListener('click', function () {
|
|
171
|
+
try { localStorage.setItem(SNOOZE_KEY, String(Date.now())) } catch (e) {}
|
|
172
|
+
el.remove()
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (pushSupported || (isIOS && !isStandalone)) {
|
|
177
|
+
var snoozedAt = 0
|
|
178
|
+
try { snoozedAt = Number(localStorage.getItem(SNOOZE_KEY) || 0) } catch (e) {}
|
|
179
|
+
// Migrate the old permanent dismissal to a snooze so previously dismissed
|
|
180
|
+
// devices get another chance instead of being silenced forever.
|
|
181
|
+
try {
|
|
182
|
+
if (localStorage.getItem('dsh-pwa-notif-hint')) { localStorage.removeItem('dsh-pwa-notif-hint'); snoozedAt = 0 }
|
|
183
|
+
} catch (e) {}
|
|
184
|
+
if (pushSupported && window.Notification.permission === 'granted') resyncPush()
|
|
185
|
+
else if (Date.now() - snoozedAt > SNOOZE_MS) {
|
|
186
|
+
if (document.readyState === 'complete') window.__DSH_PWA__.askPush()
|
|
187
|
+
else window.addEventListener('load', function () { window.__DSH_PWA__.askPush() })
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
})()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "DeepSeek Harness Mobile",
|
|
3
|
+
"short_name": "DSH Mobile",
|
|
4
|
+
"description": "DeepSeek Harness on your phone. Secure remote access to your own DSH server.",
|
|
5
|
+
"id": "/",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"orientation": "any",
|
|
10
|
+
"background_color": "#f9fafb",
|
|
11
|
+
"theme_color": "#0f1115",
|
|
12
|
+
"categories": ["developer tools", "productivity", "ai"],
|
|
13
|
+
"lang": "zh-CN",
|
|
14
|
+
"dir": "auto",
|
|
15
|
+
"prefer_related_applications": false,
|
|
16
|
+
"shortcuts": [
|
|
17
|
+
{
|
|
18
|
+
"name": "New conversation",
|
|
19
|
+
"short_name": "New chat",
|
|
20
|
+
"url": "/",
|
|
21
|
+
"icons": [{ "src": "/pwa/icons/icon.svg", "sizes": "any", "type": "image/svg+xml" }]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"icons": [
|
|
25
|
+
{ "src": "/pwa/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
|
26
|
+
{ "src": "/pwa/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
|
27
|
+
{ "src": "/pwa/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
|
28
|
+
]
|
|
29
|
+
}
|
package/pwa/offline.html
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<meta name="theme-color" content="#0f1115">
|
|
7
|
+
<title>离线 · DSH</title>
|
|
8
|
+
<style>
|
|
9
|
+
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
|
|
10
|
+
html, body { margin: 0; padding: 0; }
|
|
11
|
+
body {
|
|
12
|
+
min-height: 100dvh; display: flex; align-items: center; justify-content: center;
|
|
13
|
+
font-family: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
14
|
+
background: radial-gradient(1100px 700px at 50% -10%, #1b2233 0%, #0f1115 55%);
|
|
15
|
+
color: #e6e8ec; padding: max(20px, env(safe-area-inset-top)) 16px;
|
|
16
|
+
-webkit-text-size-adjust: 100%;
|
|
17
|
+
}
|
|
18
|
+
.card { width: 100%; max-width: 420px; text-align: center; }
|
|
19
|
+
.dot { width: 80px; height: 80px; margin: 0 auto 20px; border-radius: 24px;
|
|
20
|
+
background: linear-gradient(135deg, #4c8dff, #7a5cff); display: flex; align-items: center;
|
|
21
|
+
justify-content: center; font-size: 34px; font-weight: 700; color: #fff; }
|
|
22
|
+
h1 { font-size: 20px; margin: 0 0 8px; }
|
|
23
|
+
p { font-size: 14px; line-height: 1.8; color: #9aa3b2; margin: 0; }
|
|
24
|
+
.spin { width: 38px; height: 38px; margin: 26px auto 0; border-radius: 50%;
|
|
25
|
+
border: 3px solid #2a2f3a; border-top-color: #4c8dff; animation: a .9s linear infinite; }
|
|
26
|
+
@keyframes a { to { transform: rotate(360deg); } }
|
|
27
|
+
</style>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="card">
|
|
31
|
+
<div class="dot">DSH</div>
|
|
32
|
+
<h1>离线 / Offline</h1>
|
|
33
|
+
<p>无法连接到 DSH 服务器。<br>请在服务器侧确认网关与 DSH 仍在运行。</p>
|
|
34
|
+
<div class="spin"></div>
|
|
35
|
+
</div>
|
|
36
|
+
<script>setInterval(() => location.reload(), 5000)</script>
|
|
37
|
+
</body>
|
|
38
|
+
</html>
|
package/pwa/sw.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/* dsh-mobile-pwa · service worker
|
|
2
|
+
* Offline-capable caching + agent-done push notifications for the DSH Web UI.
|
|
3
|
+
*
|
|
4
|
+
* Strategy (v3 — see the "new DOM + old CSS" postmortem below):
|
|
5
|
+
* - True static shell (manifest, icons, offline fallback) -> cache-first.
|
|
6
|
+
* These are the only assets whose bytes cannot change without an entry in
|
|
7
|
+
* SHELL_PATHS changing too, so caching them aggressively is safe.
|
|
8
|
+
* - Everything else same-origin (DSH's own JS/CSS bundle, plugin client
|
|
9
|
+
* bundles incl. this plugin's own app.css/inject.js/touch-gestures.js,
|
|
10
|
+
* API calls, fonts, images) -> network-first, falling back to cache only
|
|
11
|
+
* when the network fetch actually fails (offline).
|
|
12
|
+
* - SPA navigations (HTML doc) -> network-first, falling back to the last
|
|
13
|
+
* successfully loaded page when offline.
|
|
14
|
+
*
|
|
15
|
+
* v2 treated ALL JS/CSS/fonts/images as cache-first-ish (stale-while-
|
|
16
|
+
* revalidate: serve the cached copy immediately, refresh in the background).
|
|
17
|
+
* That's exactly backwards for a bundle that changes on every deploy without
|
|
18
|
+
* its filename changing: a phone that had the PWA open across a deploy kept
|
|
19
|
+
* serving yesterday's cached JS/CSS on the very next cold start, while HTML
|
|
20
|
+
* navigation (already network-first) served today's markup — a "new DOM +
|
|
21
|
+
* old CSS" mismatch (e.g. cards rendered full-bleed) that a fresh local
|
|
22
|
+
* profile never reproduces because it has nothing stale to serve. Only the
|
|
23
|
+
* handful of files in SHELL_PATHS are safe to keep cache-first.
|
|
24
|
+
*
|
|
25
|
+
* Served by the gateway at /pwa/sw.js. Registered from the injected PWA script.
|
|
26
|
+
*/
|
|
27
|
+
'use strict'
|
|
28
|
+
|
|
29
|
+
const SHELL_CACHE = 'dsh-mobile-pwa-shell-v3'
|
|
30
|
+
const RUNTIME_CACHE = 'dsh-mobile-pwa-runtime-v3'
|
|
31
|
+
|
|
32
|
+
const SHELL_PATHS = new Set([
|
|
33
|
+
'/pwa/manifest.json',
|
|
34
|
+
'/pwa/icons/icon-192.png',
|
|
35
|
+
'/pwa/icons/icon-512.png',
|
|
36
|
+
'/pwa/icons/icon-maskable-512.png',
|
|
37
|
+
'/pwa/offline.html'
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
self.addEventListener('install', (event) => {
|
|
41
|
+
event.waitUntil(
|
|
42
|
+
caches.open(SHELL_CACHE).then((cache) => cache.addAll([
|
|
43
|
+
'/pwa/manifest.json',
|
|
44
|
+
'/pwa/icons/icon-192.png',
|
|
45
|
+
'/pwa/icons/icon-512.png',
|
|
46
|
+
'/pwa/icons/icon-maskable-512.png'
|
|
47
|
+
]))
|
|
48
|
+
)
|
|
49
|
+
// Safe to activate immediately: this worker only decides how *future*
|
|
50
|
+
// fetches are served (cache-first for the shell, network-first for
|
|
51
|
+
// everything else). A tab that's already loaded keeps running the JS it
|
|
52
|
+
// already has in memory regardless of which SW version is "active" — it
|
|
53
|
+
// only starts talking to the new worker on its next fetch/navigation — so
|
|
54
|
+
// skipping the wait doesn't yank code out from under a running page. What
|
|
55
|
+
// it *does* fix: without it, an old SW can sit "waiting" indefinitely on a
|
|
56
|
+
// phone that never fully closes the PWA, which is the same staleness bug
|
|
57
|
+
// this rewrite targets, just at the SW-registration layer instead of the
|
|
58
|
+
// cache layer.
|
|
59
|
+
self.skipWaiting()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
self.addEventListener('activate', (event) => {
|
|
63
|
+
event.waitUntil(
|
|
64
|
+
caches.keys().then((keys) =>
|
|
65
|
+
Promise.all(
|
|
66
|
+
keys
|
|
67
|
+
.filter((k) => k !== SHELL_CACHE && k !== RUNTIME_CACHE)
|
|
68
|
+
.map((k) => caches.delete(k))
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
// Take over already-open tabs right away rather than waiting for their
|
|
73
|
+
// next full reload. Paired with network-first below, this means an open
|
|
74
|
+
// tab starts getting fresh JS/CSS/HTML on its very next fetch instead of
|
|
75
|
+
// continuing to hit whatever the previous worker had cached.
|
|
76
|
+
self.clients.claim()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// ---- fetch handler -------------------------------------------------------
|
|
80
|
+
self.addEventListener('fetch', (event) => {
|
|
81
|
+
const req = event.request
|
|
82
|
+
const url = new URL(req.url)
|
|
83
|
+
|
|
84
|
+
// Same-origin only; we only manage the DSH origin behind the gateway.
|
|
85
|
+
if (url.origin !== self.location.origin) return
|
|
86
|
+
|
|
87
|
+
// 1. True static shell -> cache-first.
|
|
88
|
+
if (req.method === 'GET' && SHELL_PATHS.has(url.pathname)) {
|
|
89
|
+
event.respondWith(cacheFirst(req, SHELL_CACHE))
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. HTML navigations -> network-first with offline fallback to the last
|
|
94
|
+
// successfully loaded page.
|
|
95
|
+
if (req.mode === 'navigate') {
|
|
96
|
+
event.respondWith(networkFirstNavigation(req))
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Everything else same-origin GET -> network-first. This is the DSH
|
|
101
|
+
// client bundle (JS/CSS, incl. plugin client bundles), API calls,
|
|
102
|
+
// fonts and images: all of it can change on any deploy, so the server's
|
|
103
|
+
// current copy always wins; the cache only serves it when offline.
|
|
104
|
+
if (req.method === 'GET') {
|
|
105
|
+
event.respondWith(networkFirst(req, RUNTIME_CACHE))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
async function cacheFirst(req, cacheName) {
|
|
111
|
+
const cache = await caches.open(cacheName)
|
|
112
|
+
const cached = await cache.match(req)
|
|
113
|
+
if (cached) return cached
|
|
114
|
+
const res = await fetch(req)
|
|
115
|
+
if (res && res.ok) cache.put(req, res.clone())
|
|
116
|
+
return res
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function networkFirst(req, cacheName) {
|
|
120
|
+
const cache = await caches.open(cacheName)
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(req)
|
|
123
|
+
if (res && res.ok && req.method === 'GET') cache.put(req, res.clone())
|
|
124
|
+
return res
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const cached = await cache.match(req)
|
|
127
|
+
if (cached) return cached
|
|
128
|
+
throw err
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function networkFirstNavigation(req) {
|
|
133
|
+
const cache = await caches.open(SHELL_CACHE)
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetch(req)
|
|
136
|
+
if (res && res.ok) cache.put('/pwa/offline.html', res.clone())
|
|
137
|
+
return res
|
|
138
|
+
} catch (err) {
|
|
139
|
+
const cached = await cache.match('/pwa/offline.html')
|
|
140
|
+
if (cached) return cached
|
|
141
|
+
// Last resort: offline fallback hint.
|
|
142
|
+
return new Response(
|
|
143
|
+
'<!doctype html><meta charset="utf-8"><title>离线</title><style>body{font-family:system-ui;background:#0f1115;color:#e6e8ec;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}p{max-width:24em;text-align:center;line-height:1.7}</style><script>setInterval(()=>location.reload(),4000)</script><p>无法连接到 DSH 服务器,正在重试…<br>请确认你的网关与服务仍在运行。</p>',
|
|
144
|
+
{ headers: { 'Content-Type': 'text/html; charset=utf-8' } }
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- push (agent-done) notifications ------------------------------------
|
|
150
|
+
self.addEventListener('push', (event) => {
|
|
151
|
+
let data = {}
|
|
152
|
+
try { data = event.data ? event.data.json() : {} } catch (e) { /* ignore */ }
|
|
153
|
+
const title = data.title || 'DSH 任务完成'
|
|
154
|
+
const options = {
|
|
155
|
+
body: data.body || '你的智能体已完成某一步。',
|
|
156
|
+
icon: '/pwa/icons/icon-192.png',
|
|
157
|
+
badge: '/pwa/icons/icon-192.png',
|
|
158
|
+
tag: data.tag || 'dsh-agent-done',
|
|
159
|
+
renotify: true,
|
|
160
|
+
data: data.data || {}
|
|
161
|
+
}
|
|
162
|
+
event.waitUntil(self.registration.showNotification(title, options))
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
self.addEventListener('notificationclick', (event) => {
|
|
166
|
+
event.notification.close()
|
|
167
|
+
const target = (event.notification.data && event.notification.data.url) || '/'
|
|
168
|
+
event.waitUntil(
|
|
169
|
+
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((list) => {
|
|
170
|
+
for (const client of list) {
|
|
171
|
+
if ('focus' in client) { client.navigate(target); return client.focus() }
|
|
172
|
+
}
|
|
173
|
+
return self.clients.openWindow(target)
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* dsh-mobile-pwa · touch gestures
|
|
2
|
+
* Touch-first gestures for the DSH Web UI on phones:
|
|
3
|
+
* - Pinch to resize code / markdown font size
|
|
4
|
+
*
|
|
5
|
+
* Edge-swipe back (history.back) used to live here too; removed (design
|
|
6
|
+
* doc "手势" row, 2026-08-17 fourth revision): history.back() is a no-op
|
|
7
|
+
* against this SPA's own client-side routing, and the 24px left-edge hot
|
|
8
|
+
* zone div (z-index 2147483001, touch-action:none) silently ate every touch
|
|
9
|
+
* that started there — including dsh-mobile-nav's own new left-edge
|
|
10
|
+
* swipe-back gesture (effects/gestures.ts in the dsh-web-mobile plugin),
|
|
11
|
+
* which now owns that same 24px strip and actually does something useful
|
|
12
|
+
* with it (close the open sheet/panel, or return to the session list).
|
|
13
|
+
*
|
|
14
|
+
* Pull-to-refresh used to live here too; it was removed (real-device
|
|
15
|
+
* feedback: any accidental overscroll fired a full page reload, which S1's
|
|
16
|
+
* page-stack rules always land on the conversation *list*, not back on the
|
|
17
|
+
* chat the user was in — trigger-happy and disorienting mid-conversation).
|
|
18
|
+
* app.css still disables the browser's own native overscroll/rubber-band at
|
|
19
|
+
* the document edge on its own merits, independent of this file.
|
|
20
|
+
*
|
|
21
|
+
* Loaded on every device that isn't explicitly marked "desktop" via the
|
|
22
|
+
* injected PWA script. Keeps out of the way on desktop.
|
|
23
|
+
*/
|
|
24
|
+
(function () {
|
|
25
|
+
'use strict'
|
|
26
|
+
if (window.matchMedia('(pointer: coarse)').matches === false) return
|
|
27
|
+
|
|
28
|
+
// ---- Pinch to resize font -------------------------------------------
|
|
29
|
+
const FONT_KEY = 'dsh-pwa-fontscale'
|
|
30
|
+
const SPACES = [10, 11, 12, 13, 14, 15, 16, 17, 18]
|
|
31
|
+
const root = document.documentElement
|
|
32
|
+
let baseScale = parseFloat(localStorage.getItem(FONT_KEY)) || 1
|
|
33
|
+
applyFont(baseScale)
|
|
34
|
+
|
|
35
|
+
let pinchStartDist = null
|
|
36
|
+
let pinchStartScale = baseScale
|
|
37
|
+
document.addEventListener('touchstart', (e) => {
|
|
38
|
+
if (e.touches.length === 2) {
|
|
39
|
+
pinchStartDist = Math.hypot(
|
|
40
|
+
e.touches[0].clientX - e.touches[1].clientX,
|
|
41
|
+
e.touches[0].clientY - e.touches[1].clientY
|
|
42
|
+
)
|
|
43
|
+
pinchStartScale = baseScale
|
|
44
|
+
}
|
|
45
|
+
}, { passive: true })
|
|
46
|
+
|
|
47
|
+
document.addEventListener('touchmove', (e) => {
|
|
48
|
+
if (e.touches.length !== 2 || pinchStartDist == null) return
|
|
49
|
+
e.preventDefault()
|
|
50
|
+
const d = Math.hypot(
|
|
51
|
+
e.touches[0].clientX - e.touches[1].clientX,
|
|
52
|
+
e.touches[0].clientY - e.touches[1].clientY
|
|
53
|
+
)
|
|
54
|
+
const ratio = d / pinchStartDist
|
|
55
|
+
const candidates = SPACES.map((s) => s * 0.1 * ratio)
|
|
56
|
+
const next = Math.min(1.8, Math.max(0.9, pinchStartScale * ratio))
|
|
57
|
+
// Snap to nearest discrete step to avoid jitter.
|
|
58
|
+
const snapped = snapTo(next)
|
|
59
|
+
if (snapped !== baseScale) {
|
|
60
|
+
baseScale = snapped
|
|
61
|
+
applyFont(baseScale)
|
|
62
|
+
localStorage.setItem(FONT_KEY, String(baseScale))
|
|
63
|
+
}
|
|
64
|
+
}, { passive: false })
|
|
65
|
+
|
|
66
|
+
function snapTo(v) {
|
|
67
|
+
const sorted = [...SPACES].sort((a, b) => Math.abs(a - v * 10) - Math.abs(b - v * 10))
|
|
68
|
+
return sorted[0] / 10
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
document.addEventListener('touchend', () => { pinchStartDist = null }, { passive: true })
|
|
72
|
+
|
|
73
|
+
function applyFont(scale) {
|
|
74
|
+
root.style.setProperty('--dsh-pwa-font-scale', String(scale))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add a small floating reset button when scale != 1.
|
|
78
|
+
if (baseScale !== 1) {
|
|
79
|
+
const reset = document.createElement('button')
|
|
80
|
+
reset.textContent = '字AA'
|
|
81
|
+
reset.style.cssText =
|
|
82
|
+
'position:fixed;right:max(10px,env(safe-area-inset-right));' +
|
|
83
|
+
'bottom:max(110px,calc(env(safe-area-inset-bottom) + 110px));z-index:2147483002;' +
|
|
84
|
+
'background:rgba(76,141,255,.92);color:#fff;border:0;border-radius:999px;' +
|
|
85
|
+
'padding:8px 12px;font:600 12px/1 system-ui;box-shadow:0 6px 20px rgba(0,0,0,.4)'
|
|
86
|
+
reset.addEventListener('click', () => {
|
|
87
|
+
baseScale = 1
|
|
88
|
+
applyFont(1)
|
|
89
|
+
localStorage.setItem(FONT_KEY, '1')
|
|
90
|
+
reset.remove()
|
|
91
|
+
})
|
|
92
|
+
document.body.appendChild(reset)
|
|
93
|
+
}
|
|
94
|
+
})()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Wrap the tsc-compiled CommonJS client program into the DSH browser loader
|
|
2
|
+
// shape: window.__ModuleLoader__.load({ id, factory: (require) => ... }).
|
|
3
|
+
// Relative modules are inlined with a tiny local require; platform modules
|
|
4
|
+
// (react, primitives, ...) stay as require() calls and are resolved by the
|
|
5
|
+
// host's browser module table.
|
|
6
|
+
import { mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'
|
|
7
|
+
import { dirname, join, posix } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const root = dirname(fileURLToPath(new URL('../package.json', import.meta.url)))
|
|
11
|
+
const buildDir = join(root, '.client-build')
|
|
12
|
+
const outputPath = join(root, 'lib', 'client.js')
|
|
13
|
+
|
|
14
|
+
// Walk the emit dir recursively and key each module by its forward-slash
|
|
15
|
+
// relative path (e.g. "index.js", "styles/index.js"). Nested emit dirs now
|
|
16
|
+
// appear because the CSS lives in src/client/styles/.
|
|
17
|
+
async function collectSources(dir, { rel = '' } = {}) {
|
|
18
|
+
const sources = new Map()
|
|
19
|
+
for (const entry of (await readdir(dir, { withFileTypes: true }))) {
|
|
20
|
+
const abs = join(dir, entry.name)
|
|
21
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
for (const [k, v] of await collectSources(abs, { rel: relPath })) {
|
|
24
|
+
sources.set(k, v)
|
|
25
|
+
}
|
|
26
|
+
} else if (entry.isFile() && entry.name.endsWith('.js')) {
|
|
27
|
+
sources.set(
|
|
28
|
+
relPath,
|
|
29
|
+
(await readFile(abs, 'utf8')).replace(/\n?\/\/# sourceMappingURL=.*$/u, ''),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return sources
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sources = await collectSources(buildDir)
|
|
37
|
+
|
|
38
|
+
const REQUIRE_RE = /require\("(\.[^"]+\.js)"\)/g
|
|
39
|
+
// Resolve a `./x.js` or `../x.js` require relative to its parent module to
|
|
40
|
+
// the canonical forward-slash key used in __modules (e.g. styles/index.js
|
|
41
|
+
// requires "./base.css.js" -> "styles/base.css.js"; effects/header-status.js
|
|
42
|
+
// requires "../session-dot.js" -> "session-dot.js"). posix.join already
|
|
43
|
+
// interprets the leading "./"/"../" itself, so passing the FULL relative
|
|
44
|
+
// string (not the old rel.slice(2), which only stripped "./" and silently
|
|
45
|
+
// mis-resolved "../" into a bogus same-directory path) is both correct and
|
|
46
|
+
// simpler.
|
|
47
|
+
const resolveChild = (parent, rel) => {
|
|
48
|
+
const joined = posix.join(posix.dirname(parent), rel)
|
|
49
|
+
return joined === '.' ? '' : joined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Dependency-first topological order from the entry.
|
|
53
|
+
const visited = new Set()
|
|
54
|
+
const order = []
|
|
55
|
+
const visit = (file) => {
|
|
56
|
+
if (visited.has(file)) return
|
|
57
|
+
visited.add(file)
|
|
58
|
+
const src = sources.get(file)
|
|
59
|
+
if (!src) throw new Error(`client module not found for require: ${file}`)
|
|
60
|
+
for (const match of src.matchAll(REQUIRE_RE)) {
|
|
61
|
+
visit(resolveChild(file, match[1]))
|
|
62
|
+
}
|
|
63
|
+
order.push(file)
|
|
64
|
+
}
|
|
65
|
+
visit('index.js')
|
|
66
|
+
|
|
67
|
+
const modules = order
|
|
68
|
+
.map((file) => {
|
|
69
|
+
// Rewrite each relative require to its canonical path so the runtime
|
|
70
|
+
// __localRequire (id.slice(2), entry-relative) resolves nested modules.
|
|
71
|
+
const src = sources.get(file).replace(REQUIRE_RE, (m, rel) => `require("./${resolveChild(file, rel)}")`)
|
|
72
|
+
return `__modules[${JSON.stringify(file)}] = function (require, module, exports) {\n${src}\n};`
|
|
73
|
+
})
|
|
74
|
+
.join('\n')
|
|
75
|
+
|
|
76
|
+
const wrapped = [
|
|
77
|
+
'window.__ModuleLoader__.load({ id: "dsh-zen-remote", factory: (require) => {',
|
|
78
|
+
'var __modules = {};',
|
|
79
|
+
modules,
|
|
80
|
+
'var __cache = {};',
|
|
81
|
+
'function __localRequire(id) {',
|
|
82
|
+
' if (id.charCodeAt(0) !== 46) return require(id);',
|
|
83
|
+
' id = id.slice(2);',
|
|
84
|
+
' var cached = __cache[id];',
|
|
85
|
+
' if (cached) return cached.exports;',
|
|
86
|
+
' var module = { exports: {} };',
|
|
87
|
+
' __cache[id] = module;',
|
|
88
|
+
' __modules[id](__localRequire, module, module.exports);',
|
|
89
|
+
' return module.exports;',
|
|
90
|
+
'}',
|
|
91
|
+
'var module = { exports: {} };',
|
|
92
|
+
'__modules["index.js"](__localRequire, module, module.exports);',
|
|
93
|
+
'return module.exports; } });',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n')
|
|
96
|
+
|
|
97
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
98
|
+
await writeFile(outputPath, wrapped)
|
|
99
|
+
await rm(join(root, 'lib', 'client.js.map'), { force: true })
|
|
100
|
+
await rm(buildDir, { recursive: true, force: true })
|
|
101
|
+
console.log(`client bundle written: ${outputPath} (${order.length} modules inlined)`)
|