@lyhue1991/dsh-soup 0.5.6 → 0.5.8
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/explorer-data.js +9 -6
- package/lib/client/explorer-view.js +17 -3
- package/lib/client/i18n.js +2 -0
- package/lib/client/speed-badge.js +75 -115
- package/lib/client.js +120 -138
- package/lib/client.main.js +12 -7
- package/lib/host/paths.js +10 -1
- package/lib/host/speed.js +73 -26
- package/lib/index.js +3 -1
- package/package.json +2 -1
- package/skills/debug-dsh-plugin/SKILL.md +122 -0
- package/skills/debug-dsh-plugin/agents/openai.yaml +4 -0
- package/skills/debug-dsh-plugin/scripts/cdp_probe.mjs +71 -0
- package/skills/debug-dsh-plugin/scripts/dsh_restart.sh +49 -0
- package/skills/dsh-opencode-fix/SKILL.md +281 -0
|
@@ -72,8 +72,9 @@ export function createExplorerData(ctx) {
|
|
|
72
72
|
async function loadDir(path, attempt) {
|
|
73
73
|
var res = await rpc('list', { path: path, sessionId: getActiveSessionId() })
|
|
74
74
|
// 新会话刚切换时,宿主会话注册表可能尚未纳入其 cwd——稍候重试。
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// 会话 attach(含冷会话持久化读取)可能超过 2s,放宽退避至 6 次 × 1s。
|
|
76
|
+
if ((!res || !res.ok) && /超出允许范围/.test((res && res.error) || '') && (attempt || 0) < 6) {
|
|
77
|
+
await new Promise(function (r) { setTimeout(r, 1000) })
|
|
77
78
|
return loadDir(path, (attempt || 0) + 1)
|
|
78
79
|
}
|
|
79
80
|
if (!res || !res.ok) {
|
|
@@ -230,7 +231,7 @@ export function createExplorerData(ctx) {
|
|
|
230
231
|
if (!paths.length) return 'idle'
|
|
231
232
|
autoTicking = true
|
|
232
233
|
try {
|
|
233
|
-
var res = await rpc('mtime', { paths: paths })
|
|
234
|
+
var res = await rpc('mtime', { paths: paths, sessionId: getActiveSessionId() })
|
|
234
235
|
if (!res || !res.ok) return false
|
|
235
236
|
var mtimes = res.mtimes || {}
|
|
236
237
|
var dirtyDirs = []
|
|
@@ -597,6 +598,7 @@ export function createExplorerData(ctx) {
|
|
|
597
598
|
var b64 = await fileToBase64(blob)
|
|
598
599
|
var args = {
|
|
599
600
|
dir: dir, name: f.name, data: b64,
|
|
601
|
+
sessionId: getActiveSessionId(),
|
|
600
602
|
chunk: chunked ? c + 1 : undefined,
|
|
601
603
|
}
|
|
602
604
|
var res = await rpc('upload', args)
|
|
@@ -635,8 +637,9 @@ export function createExplorerData(ctx) {
|
|
|
635
637
|
async function downloadFile(path) {
|
|
636
638
|
setState({ menu: null })
|
|
637
639
|
var res = await rpc('download', { path: path, sessionId: getActiveSessionId() })
|
|
640
|
+
// 下载同样可能落在会话切换空窗:403 时等待宿主 attach 后重试一次。
|
|
638
641
|
if (!res.ok && /超出允许范围/.test(res.error || '')) {
|
|
639
|
-
await new Promise(function (r) { setTimeout(r,
|
|
642
|
+
await new Promise(function (r) { setTimeout(r, 1000) })
|
|
640
643
|
res = await rpc('download', { path: path, sessionId: getActiveSessionId() })
|
|
641
644
|
}
|
|
642
645
|
if (!res || !res.ok) {
|
|
@@ -686,9 +689,9 @@ export function createExplorerData(ctx) {
|
|
|
686
689
|
}
|
|
687
690
|
}
|
|
688
691
|
|
|
689
|
-
function onUploadPicker(e) {
|
|
692
|
+
function onUploadPicker(e, targetDir) {
|
|
690
693
|
var files = Array.from((e.target && e.target.files) || [])
|
|
691
|
-
uploadFiles(getState().cwd, files)
|
|
694
|
+
uploadFiles(targetDir || getState().cwd, files)
|
|
692
695
|
e.target.value = ''
|
|
693
696
|
}
|
|
694
697
|
|
|
@@ -17,6 +17,7 @@ export function createExplorerView(ctx) {
|
|
|
17
17
|
var getLayout = ctx.getLayout
|
|
18
18
|
var visibleRows = ctx.visibleRows
|
|
19
19
|
var uploadInputEl = null
|
|
20
|
+
var uploadTargetDir = null
|
|
20
21
|
var DETAILS_MIN = 300
|
|
21
22
|
var DETAILS_MAX = 520
|
|
22
23
|
var DETAILS_DEFAULT = 360
|
|
@@ -51,6 +52,18 @@ export function createExplorerView(ctx) {
|
|
|
51
52
|
var trackSession = ctx.trackSession
|
|
52
53
|
var onUploadPicker = ctx.onUploadPicker
|
|
53
54
|
var openFileInTab = ctx.openFileInTab
|
|
55
|
+
|
|
56
|
+
function chooseUploadDir(dir) {
|
|
57
|
+
uploadTargetDir = dir || getState().cwd
|
|
58
|
+
setState({ menu: null })
|
|
59
|
+
if (uploadInputEl) uploadInputEl.click()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onUploadInputChange(e) {
|
|
63
|
+
var targetDir = uploadTargetDir || getState().cwd
|
|
64
|
+
uploadTargetDir = null
|
|
65
|
+
onUploadPicker(e, targetDir)
|
|
66
|
+
}
|
|
54
67
|
// ▓▓ 区域一 · 资源管理器 · 🧅 葱(列表 UI)
|
|
55
68
|
// NameInput / Row / buildMenuItems / Menu:行渲染、多选、右键菜单、重命名/新建。
|
|
56
69
|
// ------------------------------------------------------------------
|
|
@@ -183,7 +196,8 @@ export function createExplorerView(ctx) {
|
|
|
183
196
|
}
|
|
184
197
|
if (node.type === 'directory') {
|
|
185
198
|
items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(node.path, false) } })
|
|
186
|
-
items.push({ key: 'newfolder', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_NEW_FOLDER } }), T('menu.newFolder')], onClick: function () { startNew(node.path, true) }
|
|
199
|
+
items.push({ key: 'newfolder', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_NEW_FOLDER } }), T('menu.newFolder')], onClick: function () { startNew(node.path, true) } })
|
|
200
|
+
items.push({ key: 'upload', label: T('menu.upload'), onClick: function () { chooseUploadDir(node.path) }, separatorAfter: true })
|
|
187
201
|
}
|
|
188
202
|
items.push({ key: 'copy', label: T('menu.copyPath'), onClick: function () { copyPath(node) } })
|
|
189
203
|
if (node.type !== 'directory') {
|
|
@@ -392,7 +406,7 @@ export function createExplorerView(ctx) {
|
|
|
392
406
|
}, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_NEW_FOLDER } })),
|
|
393
407
|
create('button', {
|
|
394
408
|
className: 'expl-btn',
|
|
395
|
-
onClick: function () {
|
|
409
|
+
onClick: function () { chooseUploadDir(getState().cwd) },
|
|
396
410
|
title: T('explorer.upload'),
|
|
397
411
|
'aria-label': T('explorer.upload'),
|
|
398
412
|
}, '⬆'),
|
|
@@ -431,7 +445,7 @@ export function createExplorerView(ctx) {
|
|
|
431
445
|
)
|
|
432
446
|
}),
|
|
433
447
|
) : null,
|
|
434
|
-
create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange:
|
|
448
|
+
create('input', { type: 'file', multiple: true, style: { display: 'none' }, ref: function (el) { uploadInputEl = el }, onChange: onUploadInputChange }),
|
|
435
449
|
s.error ? create('div', { className: 'expl-error' }, s.error) : null,
|
|
436
450
|
s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
|
|
437
451
|
s.selected.size > 1
|
package/lib/client/i18n.js
CHANGED
|
@@ -56,6 +56,7 @@ export var DICT = {
|
|
|
56
56
|
'menu.trashMulti': '🗑 移到废纸篓 ({n} 项)',
|
|
57
57
|
'menu.newFile': '📄 新建文件',
|
|
58
58
|
'menu.newFolder': '新建文件夹',
|
|
59
|
+
'menu.upload': '⬆ 上传文件',
|
|
59
60
|
'menu.copyPath': '⧉ 复制路径',
|
|
60
61
|
'menu.download': '⬇ 下载',
|
|
61
62
|
'menu.deselect': '取消选择',
|
|
@@ -145,6 +146,7 @@ export var DICT = {
|
|
|
145
146
|
'menu.trashMulti': '🗑 Move to trash ({n})',
|
|
146
147
|
'menu.newFile': '📄 New file',
|
|
147
148
|
'menu.newFolder': 'New folder',
|
|
149
|
+
'menu.upload': '⬆ Upload files',
|
|
148
150
|
'menu.copyPath': '⧉ Copy path',
|
|
149
151
|
'menu.download': '⬇ Download',
|
|
150
152
|
'menu.deselect': 'Clear selection',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 速度徽标组件(区域三 · 🧂
|
|
2
|
+
* 速度徽标组件(区域三 · 🧂 盐):会话标题旁的实时 t/s 吞吐徽标。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 挂载点:slots 槽位 conversation.session.header.actions(与官方 Agent
|
|
5
|
+
* 预设标签同槽,order 更大排在其后),React 状态驱动渲染,无 DOM 注入、
|
|
6
|
+
* 无 MutationObserver——不受消息流内部结构与文案变动影响。
|
|
6
7
|
* timerRef 由 apply 生命周期赋值,经 getTimerRef 包装函数取当前值。
|
|
7
8
|
*/
|
|
8
9
|
|
|
@@ -10,23 +11,32 @@
|
|
|
10
11
|
* @param {object} ctx - { React, T, rpc, getTimerRef }。
|
|
11
12
|
*/
|
|
12
13
|
export function createSpeedBadge({ React, T, rpc, getTimerRef }) {
|
|
14
|
+
var create = React.createElement
|
|
15
|
+
|
|
13
16
|
function SpeedBadge(props) {
|
|
14
|
-
|
|
17
|
+
// Hook 一律无条件调用(条件调用会在重渲染时打破 hook 链,
|
|
18
|
+
// 触发宿主 React 内部 RangeError 并让整个槽位条目崩溃卸载)。
|
|
19
|
+
var sessionId = props && props.sessionId
|
|
20
|
+
var useSessions = props && props.useSessions
|
|
21
|
+
var fallbackId = useSessions ? useSessions(function (list) {
|
|
22
|
+
var ids = list && list.ids
|
|
23
|
+
return ids && ids.length === 1 ? ids[0] : undefined
|
|
24
|
+
}) : undefined
|
|
25
|
+
if (!sessionId) sessionId = fallbackId
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
var
|
|
18
|
-
var
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
var
|
|
27
|
+
var st = React.useState(function () { return { phase: 'idle' } })
|
|
28
|
+
var status = st[0]
|
|
29
|
+
var setStatus = st[1]
|
|
30
|
+
var dt = React.useState(function () { return { dots: 1 } })
|
|
31
|
+
var dots = dt[0].dots
|
|
32
|
+
var setDots = dt[1]
|
|
22
33
|
|
|
23
|
-
//
|
|
34
|
+
// 省略号循环(仅 waiting 阶段有视觉意义)
|
|
24
35
|
React.useEffect(function () {
|
|
25
36
|
var timerRef = getTimerRef()
|
|
26
37
|
if (!timerRef) return
|
|
27
38
|
var stop = timerRef.interval(function () {
|
|
28
|
-
|
|
29
|
-
if (hostRef.current) renderBadge()
|
|
39
|
+
setDots(function (prev) { return prev >= 3 ? 1 : prev + 1 })
|
|
30
40
|
}, 400)
|
|
31
41
|
return function () { stop() }
|
|
32
42
|
}, [])
|
|
@@ -39,10 +49,7 @@ export function createSpeedBadge({ React, T, rpc, getTimerRef }) {
|
|
|
39
49
|
var cancelled = false
|
|
40
50
|
var poll = function () {
|
|
41
51
|
rpc('speed-status', { sessionId: sessionId }).then(function (res) {
|
|
42
|
-
if (!cancelled && res && res.ok)
|
|
43
|
-
statusRef.current = res
|
|
44
|
-
if (hostRef.current) renderBadge()
|
|
45
|
-
}
|
|
52
|
+
if (!cancelled && res && res.ok) setStatus(res)
|
|
46
53
|
}).catch(function () {})
|
|
47
54
|
}
|
|
48
55
|
poll()
|
|
@@ -50,110 +57,63 @@ export function createSpeedBadge({ React, T, rpc, getTimerRef }) {
|
|
|
50
57
|
return function () { cancelled = true; stop() }
|
|
51
58
|
}, [sessionId])
|
|
52
59
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (!el) return
|
|
57
|
-
var st = statusRef.current
|
|
58
|
-
// 清空
|
|
59
|
-
while (el.firstChild) el.removeChild(el.firstChild)
|
|
60
|
-
if (!st || st.phase === 'idle' || st.phase === 'done') {
|
|
61
|
-
el.style.display = 'none'
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
el.style.display = 'inline-flex'
|
|
65
|
-
if (st.phase === 'waiting') {
|
|
66
|
-
// 宿主 .turnStatus 用 background-clip:text + 渐变透明色,内部子元素
|
|
67
|
-
// 会继承 text-fill-color 而把颜色冲成渐变;必须 important 覆盖。
|
|
68
|
-
el.style.cssText = 'display:inline-flex;align-items:center;margin-left:10px;font-weight:400;font-size:13px;color:var(--dsw-alias-label-caption);'
|
|
69
|
-
el.style.setProperty('color', 'var(--dsw-alias-label-caption)', 'important')
|
|
70
|
-
el.style.setProperty('-webkit-text-fill-color', 'var(--dsw-alias-label-caption)', 'important')
|
|
71
|
-
el.textContent = T('speed.waiting') + new Array(dotsRef.current + 1).join('.')
|
|
72
|
-
return
|
|
73
|
-
}
|
|
74
|
-
// 流式阶段:token 计数立即显示;瞬时速率窗口未满(tps=0)时先不显示徽标,
|
|
75
|
-
// 而不是回退到「正在等待模型」(否则短输出全程都显示等待)。
|
|
76
|
-
var tps = st.tps || 0
|
|
77
|
-
var bg = tps >= 50 ? '#53b3cb' : tps >= 30 ? '#9bc53d' : tps >= 15 ? '#f9c22e' : '#e01a4f'
|
|
78
|
-
el.style.cssText = 'display:inline-flex;align-items:center;gap:6px;margin-left:10px;font-weight:400;font-size:11px;color:var(--dsw-alias-label-primary);-webkit-text-fill-color:var(--dsw-alias-label-primary);'
|
|
79
|
-
var tok = document.createElement('span')
|
|
80
|
-
tok.style.cssText = 'display:inline-flex;align-items:center;gap:4px;color:var(--dsw-alias-label-caption);-webkit-text-fill-color:var(--dsw-alias-label-caption);'
|
|
81
|
-
var SVG = 'http://www.w3.org/2000/svg'
|
|
82
|
-
var svg = document.createElementNS(SVG, 'svg')
|
|
83
|
-
svg.setAttribute('width', '10'); svg.setAttribute('height', '10'); svg.setAttribute('viewBox', '0 0 10 10')
|
|
84
|
-
svg.setAttribute('fill', 'none'); svg.setAttribute('stroke', 'currentColor'); svg.setAttribute('stroke-width', '1.2')
|
|
85
|
-
svg.setAttribute('stroke-linecap', 'round'); svg.setAttribute('stroke-linejoin', 'round')
|
|
86
|
-
var ln = document.createElementNS(SVG, 'line'); ln.setAttribute('x1', '5'); ln.setAttribute('y1', '1.5'); ln.setAttribute('x2', '5'); ln.setAttribute('y2', '8.5'); svg.appendChild(ln)
|
|
87
|
-
var poly = document.createElementNS(SVG, 'polyline'); poly.setAttribute('points', '2 6 5 8.5 8 6'); svg.appendChild(poly)
|
|
88
|
-
tok.appendChild(svg)
|
|
89
|
-
tok.appendChild(document.createTextNode(String(Math.round(st.tokens))))
|
|
90
|
-
el.appendChild(tok)
|
|
91
|
-
if (tps > 0) {
|
|
92
|
-
var pill = document.createElement('span')
|
|
93
|
-
pill.style.cssText = 'margin-left:6px;padding:1px 6px;border-radius:4px;background:' + bg + ';color:#fff;-webkit-text-fill-color:#fff;font-size:11px;font-weight:500;'
|
|
94
|
-
pill.textContent = tps.toFixed(1) + ' t/s'
|
|
95
|
-
el.appendChild(pill)
|
|
96
|
-
}
|
|
60
|
+
// idle/done 不渲染任何可见内容;保留隐藏占位便于 e2e 验证挂载。
|
|
61
|
+
if (!status || status.phase === 'idle' || status.phase === 'done') {
|
|
62
|
+
return create('span', { className: 'dsh-soup-speed', style: { display: 'none' }, 'data-phase': 'idle' })
|
|
97
63
|
}
|
|
98
64
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
function findTurnStatus() {
|
|
111
|
-
var candidates = document.querySelectorAll('[data-chat-flow] [role="status"], [data-chat-flow] [aria-live="polite"]')
|
|
112
|
-
for (var i = 0; i < candidates.length; i++) {
|
|
113
|
-
var text = candidates[i].textContent || ''
|
|
114
|
-
// DSH i18n(locale chat.deepDiving):英文 'Deep diving...' / 中文 '深度求索中...'
|
|
115
|
-
if (text.indexOf('Deep diving') !== -1 || text.indexOf('深度求索') !== -1) return candidates[i]
|
|
116
|
-
}
|
|
117
|
-
return null
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function attach() {
|
|
121
|
-
// 已挂载且宿主仍在 DOM 中时无需重复查找:徽标内容由 timer/poll 驱动
|
|
122
|
-
// renderBadge 更新,observer 只负责把徽标挂进/移出宿主元素。
|
|
123
|
-
if (hostRef.current && hostRef.current.isConnected) {
|
|
124
|
-
// 若 DSH 在徽标之后又追加了子元素(如 15s 后的 elapsed clock),
|
|
125
|
-
// 把徽标移到末尾,让它始终紧跟 Deep diving 的计时。
|
|
126
|
-
if (badge.parentNode !== hostRef.current || hostRef.current.lastElementChild !== badge) {
|
|
127
|
-
try { hostRef.current.appendChild(badge) } catch (e) {}
|
|
128
|
-
}
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
var target = findTurnStatus()
|
|
132
|
-
if (target !== null) {
|
|
133
|
-
hostRef.current = target
|
|
134
|
-
if (badge.parentNode !== target) { try { target.appendChild(badge) } catch (e) {} }
|
|
135
|
-
renderBadge()
|
|
136
|
-
} else if (hostRef.current !== null) {
|
|
137
|
-
if (badge.parentNode) { try { badge.parentNode.removeChild(badge) } catch (e) {} }
|
|
138
|
-
hostRef.current = null
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
var mo = new MutationObserver(function () { attach() })
|
|
143
|
-
mo.observe(document.body, { childList: true, subtree: true })
|
|
144
|
-
attach()
|
|
145
|
-
return function () {
|
|
146
|
-
mo.disconnect()
|
|
147
|
-
if (badge.parentNode) { try { badge.parentNode.removeChild(badge) } catch (e) {} }
|
|
148
|
-
hostRef.current = null
|
|
149
|
-
}
|
|
150
|
-
}, [])
|
|
65
|
+
if (status.phase === 'waiting') {
|
|
66
|
+
return create('span', {
|
|
67
|
+
className: 'dsh-soup-speed',
|
|
68
|
+
style: {
|
|
69
|
+
display: 'inline-flex', alignItems: 'center',
|
|
70
|
+
fontSize: '12px', fontWeight: 400,
|
|
71
|
+
color: 'var(--dsw-alias-label-caption)',
|
|
72
|
+
whiteSpace: 'nowrap',
|
|
73
|
+
},
|
|
74
|
+
}, T('speed.waiting') + '...'.slice(0, dots))
|
|
75
|
+
}
|
|
151
76
|
|
|
152
|
-
|
|
77
|
+
// 流式阶段:token 计数立即显示;瞬时速率窗口未满(tps=0)时先不显示
|
|
78
|
+
// 速率药丸,而不是回退到等待文案(否则短输出全程都显示等待)。
|
|
79
|
+
var tps = status.tps || 0
|
|
80
|
+
var bg = tps >= 50 ? '#53b3cb' : tps >= 30 ? '#9bc53d' : tps >= 15 ? '#f9c22e' : '#e01a4f'
|
|
81
|
+
var children = [
|
|
82
|
+
create('span', {
|
|
83
|
+
key: 'tok',
|
|
84
|
+
style: {
|
|
85
|
+
display: 'inline-flex', alignItems: 'center', gap: '3px',
|
|
86
|
+
color: 'var(--dsw-alias-label-caption)', fontSize: '11px',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
create('svg', {
|
|
90
|
+
width: '10', height: '10', viewBox: '0 0 10 10', fill: 'none',
|
|
91
|
+
stroke: 'currentColor', strokeWidth: '1.2',
|
|
92
|
+
strokeLinecap: 'round', strokeLinejoin: 'round',
|
|
93
|
+
},
|
|
94
|
+
create('line', { x1: '5', y1: '1.5', x2: '5', y2: '8.5' }),
|
|
95
|
+
create('polyline', { points: '2 6 5 8.5 8 6' }),
|
|
96
|
+
),
|
|
97
|
+
String(Math.round(status.tokens || 0)),
|
|
98
|
+
),
|
|
99
|
+
]
|
|
100
|
+
if (tps > 0) {
|
|
101
|
+
children.push(create('span', {
|
|
102
|
+
key: 'pill',
|
|
103
|
+
style: {
|
|
104
|
+
padding: '1px 6px', borderRadius: '4px',
|
|
105
|
+
background: bg, color: '#fff',
|
|
106
|
+
fontSize: '11px', fontWeight: 500,
|
|
107
|
+
},
|
|
108
|
+
}, tps.toFixed(1) + ' t/s'))
|
|
109
|
+
}
|
|
110
|
+
return create('span', {
|
|
111
|
+
className: 'dsh-soup-speed',
|
|
112
|
+
style: { display: 'inline-flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap' },
|
|
113
|
+
}, children)
|
|
153
114
|
}
|
|
154
115
|
|
|
155
116
|
return SpeedBadge
|
|
156
117
|
}
|
|
157
118
|
|
|
158
119
|
if (typeof window !== 'undefined') window.__DSH_SOUP_SPEED_BADGE__ = { createSpeedBadge }
|
|
159
|
-
|