@lyhue1991/dsh-soup 0.5.5 → 0.5.7
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 +6 -1
- package/lib/client/preview-renderers.js +24 -2
- package/lib/client/speed-badge.js +75 -115
- package/lib/client.js +156 -141
- package/lib/client.main.js +13 -7
- package/lib/host/paths.js +10 -1
- package/lib/host/speed.js +35 -11
- 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': '取消选择',
|
|
@@ -65,6 +66,7 @@ export var DICT = {
|
|
|
65
66
|
'files.readFail': '读取文件失败',
|
|
66
67
|
'files.reload': '重载',
|
|
67
68
|
'files.reloadTitle': '从磁盘重新读取(刷新预览)',
|
|
69
|
+
'files.downloadTitle': '下载当前文件',
|
|
68
70
|
'files.binary': '二进制文件({size}),请在资源管理器中用系统打开',
|
|
69
71
|
'files.imageTooLarge': '图片过大({size},上限 {limit})',
|
|
70
72
|
'files.pdfTooLarge': 'PDF 过大({size},上限 {limit}),请用系统打开',
|
|
@@ -86,6 +88,7 @@ export var DICT = {
|
|
|
86
88
|
'files.notEditable': '当前文件类型不支持编辑',
|
|
87
89
|
'files.truncatedEdit': '文件超过 8 MB,已截断,不能编辑保存。',
|
|
88
90
|
'files.jsonFail': 'JSON 解析失败,按纯文本显示',
|
|
91
|
+
'files.jsonTruncated': 'JSON 文件过大,仅预览前 {n} 个字符;完整内容请用编辑器打开。',
|
|
89
92
|
'files.nbFail': 'ipynb 解析失败,按纯文本显示',
|
|
90
93
|
'files.nbEmpty': '(空笔记本)',
|
|
91
94
|
'files.nbCells': '共 {n} 个单元格',
|
|
@@ -143,6 +146,7 @@ export var DICT = {
|
|
|
143
146
|
'menu.trashMulti': '🗑 Move to trash ({n})',
|
|
144
147
|
'menu.newFile': '📄 New file',
|
|
145
148
|
'menu.newFolder': 'New folder',
|
|
149
|
+
'menu.upload': '⬆ Upload files',
|
|
146
150
|
'menu.copyPath': '⧉ Copy path',
|
|
147
151
|
'menu.download': '⬇ Download',
|
|
148
152
|
'menu.deselect': 'Clear selection',
|
|
@@ -152,6 +156,7 @@ export var DICT = {
|
|
|
152
156
|
'files.readFail': 'Failed to read file',
|
|
153
157
|
'files.reload': 'Reload',
|
|
154
158
|
'files.reloadTitle': 'Re-read from disk (refresh preview)',
|
|
159
|
+
'files.downloadTitle': 'Download this file',
|
|
155
160
|
'files.binary': 'Binary file ({size}) — open with the system explorer',
|
|
156
161
|
'files.imageTooLarge': 'Image too large ({size}, limit {limit})',
|
|
157
162
|
'files.pdfTooLarge': 'PDF too large ({size}, limit {limit}) — open with the system viewer',
|
|
@@ -173,6 +178,7 @@ export var DICT = {
|
|
|
173
178
|
'files.notEditable': 'Editing is not supported for this file type',
|
|
174
179
|
'files.truncatedEdit': 'The file exceeds 8 MB and was truncated, so it cannot be safely edited.',
|
|
175
180
|
'files.jsonFail': 'JSON parse failed — shown as plain text',
|
|
181
|
+
'files.jsonTruncated': 'JSON file is too large — showing the first {n} characters; open the full content in an editor.',
|
|
176
182
|
'files.nbFail': 'ipynb parse failed — shown as plain text',
|
|
177
183
|
'files.nbEmpty': '(empty notebook)',
|
|
178
184
|
'files.nbCells': '{n} cells',
|
|
@@ -189,4 +195,3 @@ export var DICT = {
|
|
|
189
195
|
},
|
|
190
196
|
}
|
|
191
197
|
/** 模块级翻译函数(apply 里由 ctx.locale.bind 填充)。 */
|
|
192
|
-
|
|
@@ -109,6 +109,7 @@ export function createPreviewRenderers(ctx) {
|
|
|
109
109
|
var startEditing = ctx.startEditing
|
|
110
110
|
var stopEditing = ctx.stopEditing
|
|
111
111
|
var reloadFile = ctx.reloadFile
|
|
112
|
+
var downloadFile = ctx.downloadFile
|
|
112
113
|
|
|
113
114
|
function codeMirrorLanguage(path) {
|
|
114
115
|
var ext = extOfName(path)
|
|
@@ -232,6 +233,8 @@ export function createPreviewRenderers(ctx) {
|
|
|
232
233
|
|
|
233
234
|
/** 超过该字符数的代码文件不做高亮(Shiki 首次高亮的耗时保护)。 */
|
|
234
235
|
var CODE_HIGHLIGHT_MAX_CHARS = 400000
|
|
236
|
+
/** 超过该字符数的 JSON 直接按纯文本预览,避免解析和行号 DOM 开销。 */
|
|
237
|
+
var JSON_PREVIEW_MAX_CHARS = 300000
|
|
235
238
|
|
|
236
239
|
/** 把 pretty JSON 切成着色 token 数组(纯文本/React 节点,无 innerHTML)。 */
|
|
237
240
|
function jsonColorNodes(pretty) {
|
|
@@ -710,11 +713,19 @@ export function createPreviewRenderers(ctx) {
|
|
|
710
713
|
var text = entry.dirty ? entry.draft : entry.content
|
|
711
714
|
var pk = previewKind(entry.path)
|
|
712
715
|
var view = null
|
|
716
|
+
var jsonPreviewTruncated = false
|
|
713
717
|
if (pk === 'markdown') view = create(MarkdownPreview, { text: text, baseDir: parentOf(entry.path) })
|
|
714
718
|
else if (pk === 'html') view = create(HtmlPreview, {
|
|
715
719
|
text: text, path: entry.path, sessionId: getActiveSessionId(), title: entry.name,
|
|
716
720
|
})
|
|
717
|
-
else if (pk === 'json')
|
|
721
|
+
else if (pk === 'json') {
|
|
722
|
+
if (text.length > JSON_PREVIEW_MAX_CHARS) {
|
|
723
|
+
jsonPreviewTruncated = true
|
|
724
|
+
view = create('pre', { className: 'dfv-json dfv-code' }, text.slice(0, JSON_PREVIEW_MAX_CHARS))
|
|
725
|
+
} else {
|
|
726
|
+
view = create(CodeWithLines, { text: text }, create(JsonPreview, { text: text }))
|
|
727
|
+
}
|
|
728
|
+
}
|
|
718
729
|
else if (pk === 'notebook') view = create(NotebookPreview, { text: text })
|
|
719
730
|
else if (pk === 'csv' || pk === 'tsv') view = create(DelimitedPreview, { text: text, delim: pk === 'tsv' ? '\t' : ',' })
|
|
720
731
|
else {
|
|
@@ -731,6 +742,8 @@ export function createPreviewRenderers(ctx) {
|
|
|
731
742
|
return create(React.Fragment, null,
|
|
732
743
|
entry.truncated ? create('div', { className: 'dfv-cap' },
|
|
733
744
|
T('files.truncated')) : null,
|
|
745
|
+
jsonPreviewTruncated ? create('div', { className: 'dfv-cap' },
|
|
746
|
+
T('files.jsonTruncated', { n: JSON_PREVIEW_MAX_CHARS })) : null,
|
|
734
747
|
view,
|
|
735
748
|
)
|
|
736
749
|
}
|
|
@@ -762,6 +775,16 @@ export function createPreviewRenderers(ctx) {
|
|
|
762
775
|
))
|
|
763
776
|
}
|
|
764
777
|
}
|
|
778
|
+
if (active.loaded && !active.editing && !active.dirty && !active.saving) {
|
|
779
|
+
buttons.push(create('button', {
|
|
780
|
+
type: 'button',
|
|
781
|
+
className: 'dfv-btn',
|
|
782
|
+
title: T('files.downloadTitle'),
|
|
783
|
+
onClick: function () { downloadFile(active.path) },
|
|
784
|
+
},
|
|
785
|
+
T('menu.download'),
|
|
786
|
+
))
|
|
787
|
+
}
|
|
765
788
|
buttons.push(active.loaded ? create('button', {
|
|
766
789
|
type: 'button',
|
|
767
790
|
className: 'dfv-btn',
|
|
@@ -799,4 +822,3 @@ export function createPreviewRenderers(ctx) {
|
|
|
799
822
|
}
|
|
800
823
|
|
|
801
824
|
if (typeof window !== 'undefined') window.__DSH_SOUP_PREVIEW_RENDERERS__ = { createPreviewRenderers }
|
|
802
|
-
|
|
@@ -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
|
-
|