@lyhue1991/dsh-soup 0.3.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/lib/client.js +264 -79
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -97,7 +97,10 @@ dsh plugin add @lyhue1991/dsh-soup
97
97
 
98
98
  ## 更新日志
99
99
 
100
- - **0.3.0(当前)** —
100
+ - **0.4.0(当前)** —
101
+ - **语言适配(i18n)**:dsh-soup 全部文案(资源管理器、预览页、右键菜单、上传/下载、速度徽标、GoalBar)跟随 DSH 通用设置的语言切换(zh/en),切换即时生效。
102
+ - **预览 tab 纯阅读模式**:激活预览时隐藏底部输入框、「N 轮 · M 步」统计行、GoalBar 与速度徽标——整屏干净内容区,切回对话/轨迹恢复。
103
+ - **0.3.0** —
101
104
  - **Markdown 预览渲染相对路径图片**:`![x](view.jpg)` 经同源 `/api/dsh-soup/img` 端点内联加载(confine 围栏 + 仅图片类型 + 拒跨站)。
102
105
  - **自动刷新**:资源管理器与预览页每 3 秒轻量探测目录/文件签名(mtime),变了才重拉;面板关闭且无预览时零开销;宿主不可达时指数退避(封顶 60s)。
103
106
  - **顶部 ⟳ 刷新按钮**:重拉整棵树并保持已展开子目录不收起;空白右键「刷新」项同步升级为同一枚图标。
package/lib/client.js CHANGED
@@ -180,7 +180,13 @@ window.__ModuleLoader__.load({
180
180
  '.dfv-table-wrap{flex:1;min-height:0;overflow:auto;}' +
181
181
  '.dfv-table{border-collapse:collapse;font-size:12px;font-family:var(--ds-font-family-code);}' +
182
182
  '.dfv-th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:600;text-align:left;padding:5px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);border-right:1px solid var(--dsw-alias-border-l1);white-space:nowrap;}' +
183
- '.dfv-td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-width:360px;}'
183
+ '.dfv-td{padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);border-right:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;max-width:360px;}' +
184
+ // 纯阅读模式:预览 tab 激活时隐藏整个输入区(composerSeat:输入框 +
185
+ // GoalBar + 速度徽标所在的 input.dock)与轮次统计行(StatsLine)。
186
+ // class 来自 DSH 客户端编译产物(CSS module hash),升级后若失效仅
187
+ // 表现为「预览页仍显示输入区」,不影响其它功能——属可接受的降级。
188
+ '[data-dsh-soup-preview="1"] .wSkVaW_composerSeat{display:none !important;}' +
189
+ '[data-dsh-soup-preview="1"] .FJxK0a_root{display:none !important;}'
184
190
  if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css="@lyhue1991/dsh-soup/fileview.css"]') === null) {
185
191
  var fileviewTag = document.createElement('style')
186
192
  fileviewTag.dataset.plugin = '@lyhue1991/dsh-soup'
@@ -189,8 +195,146 @@ window.__ModuleLoader__.load({
189
195
  document.head.appendChild(fileviewTag)
190
196
  }
191
197
 
192
- /** 需要的客户端服务:slots、layout、timer、sessions + remote.goals(goal 动作动词)。 */
193
- var inject = ['slots', 'layout', 'timer', 'sessions', 'remote', 'remote.goals']
198
+ /** 需要的客户端服务:slots、layout、timer、sessions、locale(i18n 词典)+ remote.goals(goal 动作动词)。 */
199
+ var inject = ['slots', 'layout', 'timer', 'sessions', 'locale', 'remote', 'remote.goals']
200
+
201
+ // ------------------------------------------------------------------
202
+ // i18n:走 DSH 官方插件词典机制(ui-cordis / ui-conversation 同款)——
203
+ // ctx.locale.register(NS, DICT) 注册;槽位配置加 locale: NS 后组件 props
204
+ // 获得 t(语言切换铸造新 t 引用,靠浅比较自动重渲染整棵子树);组件外
205
+ // 回调(菜单 label、错误横幅)用模块级 T(bind 的 t 每次调用解析当前语言)。
206
+ // ------------------------------------------------------------------
207
+ var NS = 'dsh-soup'
208
+ var DICT = {
209
+ zh: {
210
+ 'explorer.title': '📁 资源管理器',
211
+ 'explorer.label': '资源管理器',
212
+ 'explorer.refresh': '刷新',
213
+ 'explorer.refreshTitle': '刷新(保持已展开目录)',
214
+ 'explorer.upload': '上传文件',
215
+ 'explorer.close': '关闭',
216
+ 'explorer.closePanel': '关闭资源管理器',
217
+ 'explorer.emptyDir': '(空目录)',
218
+ 'explorer.bulkSelected': '已选 {n} 项 · 拖到文件夹可移动',
219
+ 'explorer.readFail': '读取失败',
220
+ 'explorer.actionFail': '失败',
221
+ 'explorer.uploading': '上传中 ',
222
+ 'explorer.downloading': '下载中 ',
223
+ 'explorer.uploaded': '已上传 {n} 个文件',
224
+ 'explorer.filesCount': '{n} 个文件',
225
+ 'explorer.filesCountPartial': '{ok}/{total} 个文件',
226
+ 'explorer.downloaded': '已下载 {n} 个文件',
227
+ 'explorer.uploadFail': '上传失败',
228
+ 'explorer.downloadFail': '下载失败',
229
+ 'explorer.downloadFailWith': '下载失败: {reason}',
230
+ 'explorer.downloadStart': '已开始下载 {name}(大文件,由浏览器接管)',
231
+ 'explorer.renamed': '已复制路径',
232
+ 'explorer.renameFail': '重命名失败',
233
+ 'explorer.createFail': '创建失败',
234
+ 'explorer.moveIntoSelf': '不能把文件夹移入自身: {path}',
235
+ 'explorer.hostUnreachable': '宿主不可达: {reason}',
236
+ 'explorer.hostBadResponse': '宿主响应解析失败',
237
+ 'menu.preview': '👁 预览',
238
+ 'menu.open': '🖥 用系统打开',
239
+ 'menu.rename': '✎ 重命名',
240
+ 'menu.trash': '🗑 移到废纸篓',
241
+ 'menu.trashMulti': '🗑 移到废纸篓 ({n} 项)',
242
+ 'menu.newFile': '📄 新建文件',
243
+ 'menu.newFolder': '📁 新建文件夹',
244
+ 'menu.copyPath': '⧉ 复制路径',
245
+ 'menu.download': '⬇ 下载',
246
+ 'menu.deselect': '取消选择',
247
+ 'files.tab': '预览',
248
+ 'files.emptyHint': '在右侧资源管理器中双击或右键「预览」文件',
249
+ 'files.loading': '加载中…',
250
+ 'files.readFail': '读取文件失败',
251
+ 'files.reload': '重载',
252
+ 'files.reloadTitle': '从磁盘重新读取(刷新预览)',
253
+ 'files.binary': '二进制文件({size}),请在资源管理器中用系统打开',
254
+ 'files.imageTooLarge': '图片过大({size},上限 {limit})',
255
+ 'files.pdfTooLarge': 'PDF 过大({size},上限 {limit}),请用系统打开',
256
+ 'files.truncated': '文件超过 2 MB,仅预览前 2 MB;完整内容请用系统编辑器打开。',
257
+ 'files.jsonFail': 'JSON 解析失败,按纯文本显示',
258
+ 'files.nbFail': 'ipynb 解析失败,按纯文本显示',
259
+ 'files.nbEmpty': '(空笔记本)',
260
+ 'files.nbCells': '共 {n} 个单元格',
261
+ 'files.csvTruncated': '共 {total} 行,仅预览前 {n} 行',
262
+ 'files.htmlFrame': 'HTML 预览',
263
+ 'files.pdfFrame': 'PDF 预览',
264
+ 'files.pdfLoading': 'PDF 加载中…',
265
+ 'files.pdfFail': 'PDF 预览失败: {reason}',
266
+ 'files.pdfUnsupported': '当前浏览器无法内嵌预览 PDF,请右键用系统打开。',
267
+ 'files.nbHtmlOut': 'HTML 输出',
268
+ 'speed.label': '速度徽标',
269
+ 'speed.waiting': '正在等待模型',
270
+ 'speed.operateFail': '操作失败',
271
+ },
272
+ en: {
273
+ 'explorer.title': '📁 Explorer',
274
+ 'explorer.label': 'Explorer',
275
+ 'explorer.refresh': 'Refresh',
276
+ 'explorer.refreshTitle': 'Refresh (keeps expanded folders)',
277
+ 'explorer.upload': 'Upload files',
278
+ 'explorer.close': 'Close',
279
+ 'explorer.closePanel': 'Close explorer',
280
+ 'explorer.emptyDir': '(empty)',
281
+ 'explorer.bulkSelected': '{n} selected · drag onto a folder to move',
282
+ 'explorer.readFail': 'Failed to read',
283
+ 'explorer.actionFail': 'Failed',
284
+ 'explorer.uploading': 'Uploading ',
285
+ 'explorer.downloading': 'Downloading ',
286
+ 'explorer.uploaded': 'Uploaded {n} file(s)',
287
+ 'explorer.filesCount': '{n} file(s)',
288
+ 'explorer.filesCountPartial': '{ok}/{total} file(s)',
289
+ 'explorer.downloaded': 'Downloaded {n} file(s)',
290
+ 'explorer.uploadFail': 'Upload failed',
291
+ 'explorer.downloadFail': 'Download failed',
292
+ 'explorer.downloadFailWith': 'Download failed: {reason}',
293
+ 'explorer.downloadStart': 'Downloading {name} (large file, handled by the browser)',
294
+ 'explorer.renamed': 'Path copied',
295
+ 'explorer.renameFail': 'Rename failed',
296
+ 'explorer.createFail': 'Create failed',
297
+ 'explorer.moveIntoSelf': 'Cannot move a folder into itself: {path}',
298
+ 'explorer.hostUnreachable': 'Host unreachable: {reason}',
299
+ 'explorer.hostBadResponse': 'Failed to parse host response',
300
+ 'menu.preview': '👁 Preview',
301
+ 'menu.open': '🖥 Open with system',
302
+ 'menu.rename': '✎ Rename',
303
+ 'menu.trash': '🗑 Move to trash',
304
+ 'menu.trashMulti': '🗑 Move to trash ({n})',
305
+ 'menu.newFile': '📄 New file',
306
+ 'menu.newFolder': '📁 New folder',
307
+ 'menu.copyPath': '⧉ Copy path',
308
+ 'menu.download': '⬇ Download',
309
+ 'menu.deselect': 'Clear selection',
310
+ 'files.tab': 'Preview',
311
+ 'files.emptyHint': 'Double-click a file (or right-click → Preview) in the explorer',
312
+ 'files.loading': 'Loading…',
313
+ 'files.readFail': 'Failed to read file',
314
+ 'files.reload': 'Reload',
315
+ 'files.reloadTitle': 'Re-read from disk (refresh preview)',
316
+ 'files.binary': 'Binary file ({size}) — open with the system explorer',
317
+ 'files.imageTooLarge': 'Image too large ({size}, limit {limit})',
318
+ 'files.pdfTooLarge': 'PDF too large ({size}, limit {limit}) — open with the system viewer',
319
+ 'files.truncated': 'File exceeds 2 MB — showing the first 2 MB; open in a system editor for the full content.',
320
+ 'files.jsonFail': 'JSON parse failed — shown as plain text',
321
+ 'files.nbFail': 'ipynb parse failed — shown as plain text',
322
+ 'files.nbEmpty': '(empty notebook)',
323
+ 'files.nbCells': '{n} cells',
324
+ 'files.csvTruncated': '{total} rows — showing the first {n}',
325
+ 'files.htmlFrame': 'HTML preview',
326
+ 'files.pdfFrame': 'PDF preview',
327
+ 'files.pdfLoading': 'Loading PDF…',
328
+ 'files.pdfFail': 'PDF preview failed: {reason}',
329
+ 'files.pdfUnsupported': 'This browser cannot embed PDF previews — right-click and open with the system viewer.',
330
+ 'files.nbHtmlOut': 'HTML output',
331
+ 'speed.label': 'Speed badge',
332
+ 'speed.waiting': 'Waiting for the model',
333
+ 'speed.operateFail': 'Action failed',
334
+ },
335
+ }
336
+ /** 模块级翻译函数(apply 里由 ctx.locale.bind 填充)。 */
337
+ var T = function (key, params) { return key }
194
338
 
195
339
  // ------------------------------------------------------------------
196
340
  // 状态存储(模块级,供 Panel 与 HeaderAction 共享)
@@ -254,9 +398,9 @@ window.__ModuleLoader__.load({
254
398
  headers: { 'content-type': 'application/json', 'x-dsh-soup': '1' },
255
399
  body: JSON.stringify({ action: action, args: args || {} }),
256
400
  }).then(function (res) {
257
- return res.json().catch(function () { return { ok: false, error: '宿主响应解析失败' } })
401
+ return res.json().catch(function () { return { ok: false, error: T('explorer.hostBadResponse') } })
258
402
  }).catch(function (err) {
259
- return { ok: false, error: '宿主不可达: ' + String((err && err.message) || err) }
403
+ return { ok: false, error: T('explorer.hostUnreachable', { reason: String((err && err.message) || err) }) }
260
404
  })
261
405
  }
262
406
 
@@ -268,7 +412,7 @@ window.__ModuleLoader__.load({
268
412
  return loadDir(path, (attempt || 0) + 1)
269
413
  }
270
414
  if (!res || !res.ok) {
271
- setState({ error: (res && res.error) || '读取失败' })
415
+ setState({ error: (res && res.error) || T('explorer.readFail') })
272
416
  return null
273
417
  }
274
418
  return res.entries || []
@@ -599,7 +743,7 @@ window.__ModuleLoader__.load({
599
743
  var errors = []
600
744
  for (var i = 0; i < paths.length; i++) {
601
745
  var res = await rpc('trash', { path: paths[i] })
602
- if (!res || !res.ok) errors.push(baseName(paths[i]) + ': ' + ((res && res.error) || '失败') + ((res && res.hint) ? '(' + res.hint + ')' : ''))
746
+ if (!res || !res.ok) errors.push(baseName(paths[i]) + ': ' + ((res && res.error) || T('explorer.actionFail')) + ((res && res.hint) ? '(' + res.hint + ')' : ''))
603
747
  }
604
748
  if (errors.length) setState({ error: errors.join(';') })
605
749
  setState({ selected: new Set(), lastIndex: null })
@@ -620,7 +764,7 @@ window.__ModuleLoader__.load({
620
764
  setState({ renaming: null, selected: sel })
621
765
  ;(async function () {
622
766
  var res = await rpc('move', { from: path, to: dest })
623
- if (!res || !res.ok) setState({ error: (res && res.error) || '重命名失败' })
767
+ if (!res || !res.ok) setState({ error: (res && res.error) || T('explorer.renameFail') })
624
768
  await refresh()
625
769
  })()
626
770
  }
@@ -641,7 +785,7 @@ window.__ModuleLoader__.load({
641
785
  setState({ newItem: null })
642
786
  ;(async function () {
643
787
  var res = await rpc('create', { dir: parent, name: name, isDir: isDir })
644
- if (!res || !res.ok) setState({ error: (res && res.error) || '创建失败' })
788
+ if (!res || !res.ok) setState({ error: (res && res.error) || T('explorer.createFail') })
645
789
  await refresh()
646
790
  })()
647
791
  }
@@ -661,7 +805,7 @@ window.__ModuleLoader__.load({
661
805
  setState({ menu: null })
662
806
  try {
663
807
  if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
664
- navigator.clipboard.writeText(p).then(function () { showNotice('已复制路径') }).catch(function () {})
808
+ navigator.clipboard.writeText(p).then(function () { showNotice(T('explorer.renamed')) }).catch(function () {})
665
809
  }
666
810
  } catch (err) {}
667
811
  }
@@ -703,9 +847,9 @@ window.__ModuleLoader__.load({
703
847
  var name = baseName(p)
704
848
  var dest = pathJoin(targetDir, name)
705
849
  if (dest === p) continue
706
- if (targetDir === p || targetDir.startsWith(p + '/')) { errors.push('不能把文件夹移入自身: ' + name); continue }
850
+ if (targetDir === p || targetDir.startsWith(p + '/')) { errors.push(T('explorer.moveIntoSelf', { path: name })); continue }
707
851
  var res = await rpc('move', { from: p, to: dest })
708
- if (!res || !res.ok) errors.push(name + ': ' + ((res && res.error) || '失败'))
852
+ if (!res || !res.ok) errors.push(name + ': ' + ((res && res.error) || T('explorer.actionFail')))
709
853
  }
710
854
  setState({ dragPaths: null })
711
855
  if (errors.length) setState({ error: errors.join(';') })
@@ -716,7 +860,7 @@ window.__ModuleLoader__.load({
716
860
  return new Promise(function (resolve, reject) {
717
861
  var r = new FileReader()
718
862
  r.onload = function () { var s = String(r.result || ''); var i = s.indexOf(','); resolve(i >= 0 ? s.slice(i + 1) : s) }
719
- r.onerror = function () { reject(new Error('读取文件失败')) }
863
+ r.onerror = function () { reject(new Error(T('files.readFail'))) }
720
864
  r.readAsDataURL(file)
721
865
  })
722
866
  }
@@ -739,9 +883,9 @@ window.__ModuleLoader__.load({
739
883
  }
740
884
 
741
885
  function finishUploads(count, okCount, failedName, down) {
742
- var verb = down ? '已下载' : '已上传'
886
+ var verb = down ? T('explorer.downloaded') : T('explorer.uploaded')
743
887
  setUploadEntry({
744
- name: '✓ ' + verb + ' ' + (okCount === count ? count + ' 个文件' : okCount + '/' + count + ' 个文件' + (failedName ? '(' + failedName + ' 失败)' : '')),
888
+ name: '✓ ' + verb + ' ' + (okCount === count ? T('explorer.filesCount', { n: count }) : T('explorer.filesCountPartial', { ok: okCount, total: count }) + (failedName ? '(' + failedName + ')' : '')),
745
889
  loaded: 0, total: 0, idx: 0, totalFiles: 0, done: true, down: !!down,
746
890
  })
747
891
  window.setTimeout(function () { setState({ uploads: [] }) }, 1600)
@@ -760,7 +904,7 @@ window.__ModuleLoader__.load({
760
904
  dir: dir, name: f.name, data: b64,
761
905
  chunk: chunked ? c + 1 : undefined,
762
906
  })
763
- if (!res || !res.ok) throw new Error((res && res.error) || '上传失败')
907
+ if (!res || !res.ok) throw new Error((res && res.error) || T('explorer.uploadFail'))
764
908
  updateUpload(f.name, Math.min(f.size, start + UPLOAD_CHUNK_SIZE), f.size, idx, totalFiles)
765
909
  }
766
910
  }
@@ -794,7 +938,7 @@ window.__ModuleLoader__.load({
794
938
  res = await rpc('download', { path: path, sessionId: activeSessionId })
795
939
  }
796
940
  if (!res || !res.ok) {
797
- setState({ error: (res && res.error) || '下载失败' })
941
+ setState({ error: (res && res.error) || T('explorer.downloadFail') })
798
942
  return
799
943
  }
800
944
  var name = res.name || baseName(path)
@@ -807,7 +951,7 @@ window.__ModuleLoader__.load({
807
951
  document.body.appendChild(a)
808
952
  a.click()
809
953
  a.remove()
810
- showNotice('已开始下载 ' + name + '(大文件,由浏览器接管)')
954
+ showNotice(T('explorer.downloadStart', { name: name }))
811
955
  return
812
956
  }
813
957
  // 中小文件:fetch 流式读字节计进度,完成后 Blob 另存(峰值≈文件体积)
@@ -836,7 +980,7 @@ window.__ModuleLoader__.load({
836
980
  window.setTimeout(function () { URL.revokeObjectURL(blobUrl) }, 5000)
837
981
  finishUploads(1, 1, '', true)
838
982
  } catch (err) {
839
- setState({ error: '下载失败: ' + String((err && err.message) || err), uploads: [] })
983
+ setState({ error: T('explorer.downloadFailWith', { reason: String((err && err.message) || err) }), uploads: [] })
840
984
  }
841
985
  }
842
986
 
@@ -852,7 +996,7 @@ window.__ModuleLoader__.load({
852
996
  // markdown/html/json/csv 有专属渲染器;图片预览;其余按纯文本。
853
997
  // 状态存模块级 store,切走 tab 再切回来内容不丢。
854
998
  // ------------------------------------------------------------------
855
- var FILES_TAB_LABEL = '预览'
999
+ var FILES_TAB_LABEL = function () { return T('files.tab') }
856
1000
 
857
1001
  /** 当前活跃会话:list/read/download 携带给宿主,供围栏直查该会话 cwd。 */
858
1002
  var activeSessionId = null
@@ -901,7 +1045,7 @@ window.__ModuleLoader__.load({
901
1045
  try {
902
1046
  var tabs = document.querySelectorAll('[role="tab"]')
903
1047
  for (var i = 0; i < tabs.length; i++) {
904
- if (tabs[i].textContent === FILES_TAB_LABEL) { tabs[i].click(); return true }
1048
+ if (tabs[i].textContent === FILES_TAB_LABEL()) { tabs[i].click(); return true }
905
1049
  }
906
1050
  } catch (err) {}
907
1051
  return false
@@ -939,7 +1083,7 @@ window.__ModuleLoader__.load({
939
1083
  if (!cur) return
940
1084
  cur.loading = false
941
1085
  if (!res || !res.ok) {
942
- cur.error = (res && res.error) || '读取失败'
1086
+ cur.error = (res && res.error) || T('explorer.readFail')
943
1087
  } else {
944
1088
  cur.error = ''
945
1089
  cur.loaded = true
@@ -1071,7 +1215,7 @@ window.__ModuleLoader__.load({
1071
1215
  }
1072
1216
  if (isDir && node.open) {
1073
1217
  if (node.loading) {
1074
- children.push(create('div', { key: '__loading__', className: 'expl-muted', style: { paddingLeft: 8 + (depth + 1) * 16 } }, '加载中…'))
1218
+ children.push(create('div', { key: '__loading__', className: 'expl-muted', style: { paddingLeft: 8 + (depth + 1) * 16 } }, T('files.loading')))
1075
1219
  } else {
1076
1220
  ;(node.children || []).forEach(function (c) { children.push(create(Row, { key: c.path, node: c, depth: depth + 1 })) })
1077
1221
  }
@@ -1086,29 +1230,29 @@ window.__ModuleLoader__.load({
1086
1230
  if (node) {
1087
1231
  if (!multi) {
1088
1232
  if (node.type !== 'directory') {
1089
- items.push({ key: 'open-tab', label: '👁 预览', onClick: function () { openFileInTab(node.path) } })
1090
- items.push({ key: 'open', label: '🖥 用系统打开', onClick: openSelection, separatorAfter: true })
1233
+ items.push({ key: 'open-tab', label: T('menu.preview'), onClick: function () { openFileInTab(node.path) } })
1234
+ items.push({ key: 'open', label: T('menu.open'), onClick: openSelection, separatorAfter: true })
1091
1235
  } else {
1092
- items.push({ key: 'open', label: '🖥 用系统打开', onClick: openSelection, separatorAfter: true })
1236
+ items.push({ key: 'open', label: T('menu.open'), onClick: openSelection, separatorAfter: true })
1093
1237
  }
1094
- items.push({ key: 'rename', label: '✎ 重命名', onClick: function () { startRename(node) } })
1095
- items.push({ key: 'trash', label: '🗑 移到废纸篓', danger: true, onClick: trashSelection, separatorAfter: true })
1238
+ items.push({ key: 'rename', label: T('menu.rename'), onClick: function () { startRename(node) } })
1239
+ items.push({ key: 'trash', label: T('menu.trash'), danger: true, onClick: trashSelection, separatorAfter: true })
1096
1240
  } else {
1097
- items.push({ key: 'trash', label: '🗑 移到废纸篓 (' + state.selected.size + ' 项)', danger: true, onClick: trashSelection, separatorAfter: true })
1241
+ items.push({ key: 'trash', label: T('menu.trashMulti', { n: state.selected.size }), danger: true, onClick: trashSelection, separatorAfter: true })
1098
1242
  }
1099
1243
  if (node.type === 'directory') {
1100
- items.push({ key: 'newfile', label: '📄 新建文件', onClick: function () { startNew(node.path, false) } })
1101
- items.push({ key: 'newfolder', label: '📁 新建文件夹', onClick: function () { startNew(node.path, true) }, separatorAfter: true })
1244
+ items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(node.path, false) } })
1245
+ items.push({ key: 'newfolder', label: T('menu.newFolder'), onClick: function () { startNew(node.path, true) }, separatorAfter: true })
1102
1246
  }
1103
- items.push({ key: 'copy', label: '⧉ 复制路径', onClick: function () { copyPath(node) } })
1247
+ items.push({ key: 'copy', label: T('menu.copyPath'), onClick: function () { copyPath(node) } })
1104
1248
  if (node.type !== 'directory') {
1105
- items.push({ key: 'download', label: '⬇ 下载', onClick: function () { downloadFile(node.path) } })
1249
+ items.push({ key: 'download', label: T('menu.download'), onClick: function () { downloadFile(node.path) } })
1106
1250
  }
1107
1251
  } else {
1108
- items.push({ key: 'newfile', label: '📄 新建文件', onClick: function () { startNew(state.cwd, false) } })
1109
- items.push({ key: 'newfolder', label: '📁 新建文件夹', onClick: function () { startNew(state.cwd, true) } })
1110
- items.push({ key: 'refresh', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }), '刷新'], onClick: refresh, separatorAfter: true })
1111
- if (state.selected.size > 0) items.push({ key: 'none', label: '取消选择', onClick: function () { setState({ selected: new Set(), lastIndex: null }) } })
1252
+ items.push({ key: 'newfile', label: T('menu.newFile'), onClick: function () { startNew(state.cwd, false) } })
1253
+ items.push({ key: 'newfolder', label: T('menu.newFolder'), onClick: function () { startNew(state.cwd, true) } })
1254
+ items.push({ key: 'refresh', label: [create('span', { className: 'expl-menu-ico', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }), T('explorer.refresh')], onClick: refresh, separatorAfter: true })
1255
+ if (state.selected.size > 0) items.push({ key: 'none', label: T('menu.deselect'), onClick: function () { setState({ selected: new Set(), lastIndex: null }) } })
1112
1256
  }
1113
1257
  return items
1114
1258
  }
@@ -1306,7 +1450,7 @@ window.__ModuleLoader__.load({
1306
1450
  className: 'dfv-frame',
1307
1451
  sandbox: 'allow-scripts allow-popups allow-forms allow-modals',
1308
1452
  referrerPolicy: 'no-referrer',
1309
- title: props.title || 'HTML 预览',
1453
+ title: props.title || T('files.htmlFrame'),
1310
1454
  srcDoc: props.text,
1311
1455
  })
1312
1456
  }
@@ -1335,14 +1479,14 @@ window.__ModuleLoader__.load({
1335
1479
  if (made) URL.revokeObjectURL(made)
1336
1480
  }
1337
1481
  }, [props.data])
1338
- if (state.error) return create('div', { className: 'dfv-error' }, 'PDF 预览失败: ' + state.error)
1339
- if (!state.url) return create('div', { className: 'dfv-empty' }, 'PDF 加载中…')
1482
+ if (state.error) return create('div', { className: 'dfv-error' }, T('files.pdfFail', { reason: state.error }))
1483
+ if (!state.url) return create('div', { className: 'dfv-empty' }, T('files.pdfLoading'))
1340
1484
  return create('object', {
1341
1485
  className: 'dfv-frame dfv-pdf',
1342
1486
  type: 'application/pdf',
1343
1487
  data: state.url,
1344
- 'aria-label': 'PDF 预览',
1345
- }, '当前浏览器无法内嵌预览 PDF,请右键用系统打开。')
1488
+ 'aria-label': T('files.pdfFrame'),
1489
+ }, T('files.pdfUnsupported'))
1346
1490
  }
1347
1491
 
1348
1492
  // ------------------------------------------------------------------
@@ -1395,7 +1539,7 @@ window.__ModuleLoader__.load({
1395
1539
  return create('img', { className: 'dfv-nb-img', src: 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(nbSource(out.data[mime])))), alt: 'notebook output svg' })
1396
1540
  }
1397
1541
  if (mime === 'text/html') {
1398
- return create('iframe', { className: 'dfv-nb-html', sandbox: '', title: 'HTML 输出', srcDoc: nbSource(out.data[mime]) })
1542
+ return create('iframe', { className: 'dfv-nb-html', sandbox: '', title: T('files.nbHtmlOut'), srcDoc: nbSource(out.data[mime]) })
1399
1543
  }
1400
1544
  if (mime === 'text/markdown' && MarkdownText) {
1401
1545
  return create('div', { className: 'dfv-md-wrap dfv-nb-md' }, create(MarkdownText, { text: nbSource(out.data[mime]) }))
@@ -1411,7 +1555,7 @@ window.__ModuleLoader__.load({
1411
1555
  try { nb = JSON.parse(props.text) } catch (err) { nb = undefined }
1412
1556
  if (nb === undefined || typeof nb !== 'object' || nb === null) {
1413
1557
  return create(React.Fragment, null,
1414
- create('div', { className: 'dfv-cap' }, 'ipynb 解析失败,按纯文本显示'),
1558
+ create('div', { className: 'dfv-cap' }, T('files.nbFail')),
1415
1559
  create('pre', { className: 'dfv-code' }, props.text))
1416
1560
  }
1417
1561
  var cells = Array.isArray(nb.cells) ? nb.cells : []
@@ -1458,8 +1602,8 @@ window.__ModuleLoader__.load({
1458
1602
 
1459
1603
  return create('div', { className: 'dfv-nb' },
1460
1604
  create('div', { className: 'dfv-cap' },
1461
- 'Jupyter Notebook' + (kernelName ? ' · ' + kernelName : '') + ' · ' + cells.length + ' 个单元格'),
1462
- cellEls.length ? cellEls : create('div', { className: 'dfv-muted' }, '(空笔记本)'))
1605
+ 'Jupyter Notebook' + (kernelName ? ' · ' + kernelName : '') + ' · ' + T('files.nbCells', { n: cells.length })),
1606
+ cellEls.length ? cellEls : create('div', { className: 'dfv-muted' }, T('files.nbEmpty')))
1463
1607
  }
1464
1608
 
1465
1609
  function JsonPreview(props) {
@@ -1467,7 +1611,7 @@ window.__ModuleLoader__.load({
1467
1611
  try { JSON.parse(props.text) } catch (err) { failed = true }
1468
1612
  if (failed) {
1469
1613
  return create(React.Fragment, null,
1470
- create('div', { className: 'dfv-cap' }, 'JSON 解析失败,按纯文本显示'),
1614
+ create('div', { className: 'dfv-cap' }, T('files.jsonFail')),
1471
1615
  create('pre', { className: 'dfv-code' }, props.text))
1472
1616
  }
1473
1617
  return create('pre', { className: 'dfv-json dfv-code' }, jsonColorNodes(JSON.stringify(JSON.parse(props.text), null, 2)))
@@ -1484,7 +1628,7 @@ window.__ModuleLoader__.load({
1484
1628
  cols = Math.max(cols, Math.min(rows[r].length, CSV_MAX_COLS))
1485
1629
  }
1486
1630
  var caps = []
1487
- if (totalRows > CSV_MAX_ROWS + 1) caps.push('共 ' + totalRows + ' 行,仅预览前 ' + CSV_MAX_ROWS + ' 行')
1631
+ if (totalRows > CSV_MAX_ROWS + 1) caps.push(T('files.csvTruncated', { total: totalRows, n: CSV_MAX_ROWS }))
1488
1632
  var trs = []
1489
1633
  for (var ri = 1; ri <= Math.min(totalRows - 1, CSV_MAX_ROWS); ri++) {
1490
1634
  var cells = rows[ri]
@@ -1520,18 +1664,18 @@ window.__ModuleLoader__.load({
1520
1664
  function FileContent(props) {
1521
1665
  var entry = props.entry
1522
1666
  if (entry.loading) {
1523
- return create('div', { className: 'dfv-empty' }, '加载中…')
1667
+ return create('div', { className: 'dfv-empty' }, T('files.loading'))
1524
1668
  }
1525
1669
  if (entry.error) {
1526
1670
  return create('div', { className: 'dfv-error' }, entry.error)
1527
1671
  }
1528
1672
  if (entry.kind === 'binary') {
1529
1673
  return create('div', { className: 'dfv-muted' },
1530
- '二进制文件(' + formatBytes(entry.size) + '),请在资源管理器中用系统打开')
1674
+ T('files.binary', { size: formatBytes(entry.size) }))
1531
1675
  }
1532
1676
  if (entry.kind === 'image-too-large') {
1533
1677
  return create('div', { className: 'dfv-muted' },
1534
- '图片过大(' + formatBytes(entry.size) + ',上限 ' + formatBytes(entry.limit) + ')')
1678
+ T('files.imageTooLarge', { size: formatBytes(entry.size), limit: formatBytes(entry.limit) }))
1535
1679
  }
1536
1680
  if (entry.kind === 'image') {
1537
1681
  return create('div', { className: 'dfv-image-wrap' },
@@ -1539,7 +1683,7 @@ window.__ModuleLoader__.load({
1539
1683
  }
1540
1684
  if (entry.kind === 'pdf-too-large') {
1541
1685
  return create('div', { className: 'dfv-muted' },
1542
- 'PDF 过大(' + formatBytes(entry.size) + ',上限 ' + formatBytes(entry.limit) + '),请用系统打开')
1686
+ T('files.pdfTooLarge', { size: formatBytes(entry.size), limit: formatBytes(entry.limit) }))
1543
1687
  }
1544
1688
  if (entry.kind === 'pdf') {
1545
1689
  return create(PdfPreview, { data: entry.data })
@@ -1565,15 +1709,51 @@ window.__ModuleLoader__.load({
1565
1709
  }
1566
1710
  return create(React.Fragment, null,
1567
1711
  entry.truncated ? create('div', { className: 'dfv-cap' },
1568
- '文件超过 2 MB,仅预览前 2 MB;完整内容请用系统编辑器打开。') : null,
1712
+ T('files.truncated')) : null,
1569
1713
  view,
1570
1714
  )
1571
1715
  }
1572
1716
 
1717
+ /**
1718
+ * 纯阅读模式标记:在会话根(含 composerSeat 的最近祖先,即 DSH
1719
+ * ConversationRoot 的 root 节点)上打 data-dsh-soup-preview,由上面
1720
+ * 注入的 CSS 隐藏输入区与轮次统计行。DSH 重渲染可能重建该节点——
1721
+ * MutationObserver 在 FilesView 存活期间持续确保标记存在。
1722
+ */
1723
+ function usePreviewReadingMode(rootRef) {
1724
+ React.useEffect(function () {
1725
+ var observer = null
1726
+ var marked = null
1727
+ function apply() {
1728
+ var el = rootRef.current
1729
+ if (!el) return
1730
+ if (marked && marked.contains(el) && marked.getAttribute('data-dsh-soup-preview') === '1') return
1731
+ var host = el.parentElement
1732
+ while (host) {
1733
+ if (host.querySelector && host.querySelector('.wSkVaW_composerSeat')) break
1734
+ host = host.parentElement
1735
+ }
1736
+ if (!host) return
1737
+ host.setAttribute('data-dsh-soup-preview', '1')
1738
+ marked = host
1739
+ }
1740
+ apply()
1741
+ observer = new MutationObserver(function () { apply() })
1742
+ observer.observe(document.body, { childList: true, subtree: true })
1743
+ return function () {
1744
+ if (observer) observer.disconnect()
1745
+ if (marked) marked.removeAttribute('data-dsh-soup-preview')
1746
+ marked = null
1747
+ }
1748
+ }, [])
1749
+ }
1750
+
1573
1751
  function FilesView(props) {
1574
1752
  var s = useStore()
1575
1753
  var files = s.files
1576
1754
  var active = files.active ? findFileEntry(files.active) : null
1755
+ var rootRef = React.useRef(null)
1756
+ usePreviewReadingMode(rootRef)
1577
1757
 
1578
1758
  var tabs = files.list.map(function (entry) {
1579
1759
  return create('button', {
@@ -1587,7 +1767,7 @@ window.__ModuleLoader__.load({
1587
1767
  create('span', {
1588
1768
  className: 'dfv-close',
1589
1769
  role: 'button',
1590
- title: '关闭',
1770
+ title: T('explorer.close'),
1591
1771
  onClick: function (e) { e.stopPropagation(); closeFileTab(entry.path) },
1592
1772
  }, '✕'),
1593
1773
  )
@@ -1597,7 +1777,7 @@ window.__ModuleLoader__.load({
1597
1777
  if (!active) {
1598
1778
  body = create('div', { className: 'dfv-empty' },
1599
1779
  create('span', { style: { fontSize: 22 } }, '📄'),
1600
- '在右侧资源管理器中双击或右键「预览」文件',
1780
+ T('files.emptyHint'),
1601
1781
  )
1602
1782
  } else {
1603
1783
  body = create('div', { className: 'dfv-body' },
@@ -1607,18 +1787,18 @@ window.__ModuleLoader__.load({
1607
1787
  active.loaded ? create('button', {
1608
1788
  type: 'button',
1609
1789
  className: 'dfv-btn',
1610
- title: '从磁盘重新读取(刷新预览)',
1790
+ title: T('files.reloadTitle'),
1611
1791
  onClick: function () { reloadFile(active.path) },
1612
1792
  },
1613
1793
  create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } }),
1614
- '重载',
1794
+ T('files.reload'),
1615
1795
  ) : null,
1616
1796
  ),
1617
1797
  create(FileContent, { key: active.path, entry: active }),
1618
1798
  )
1619
1799
  }
1620
1800
 
1621
- return create('div', { className: 'dfv-root' },
1801
+ return create('div', { className: 'dfv-root', ref: rootRef },
1622
1802
  files.list.length > 0 ? create('div', { className: 'dfv-tabbar' }, tabs) : null,
1623
1803
  body,
1624
1804
  )
@@ -1758,7 +1938,7 @@ window.__ModuleLoader__.load({
1758
1938
  ))
1759
1939
  }
1760
1940
  if (s.tree.length === 0 && !(state.newItem && state.newItem.parent === state.cwd)) {
1761
- bodyChildren.push(create('div', { key: '__empty__', className: 'expl-muted' }, '(空目录)'))
1941
+ bodyChildren.push(create('div', { key: '__empty__', className: 'expl-muted' }, T('explorer.emptyDir')))
1762
1942
  } else {
1763
1943
  s.tree.forEach(function (node) { bodyChildren.push(create(Row, { key: node.path, node: node, depth: 0 })) })
1764
1944
  }
@@ -1775,19 +1955,19 @@ window.__ModuleLoader__.load({
1775
1955
  return create('div', { ref: panelRef, className: 'expl-panel' },
1776
1956
  create('div', { className: 'expl-resize', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }),
1777
1957
  create('div', { className: 'expl-head' },
1778
- create('span', { className: 'expl-title' }, '📁 资源管理器'),
1958
+ create('span', { className: 'expl-title' }, T('explorer.title')),
1779
1959
  create('div', { className: 'expl-head-btns' },
1780
1960
  create('button', {
1781
1961
  className: 'expl-btn',
1782
1962
  onClick: function () { refreshAll() },
1783
- title: '刷新(保持已展开目录)',
1784
- 'aria-label': '刷新',
1963
+ title: T('explorer.refreshTitle'),
1964
+ 'aria-label': T('explorer.refresh'),
1785
1965
  }, create('span', { className: 'expl-icon', dangerouslySetInnerHTML: { __html: ICON_REFRESH } })),
1786
1966
  create('button', {
1787
1967
  className: 'expl-btn',
1788
1968
  onClick: function () { if (uploadInputEl) uploadInputEl.click() },
1789
- title: '上传文件',
1790
- 'aria-label': '上传文件',
1969
+ title: T('explorer.upload'),
1970
+ 'aria-label': T('explorer.upload'),
1791
1971
  }, '⬆'),
1792
1972
  create('button', {
1793
1973
  className: 'expl-btn',
@@ -1796,8 +1976,8 @@ window.__ModuleLoader__.load({
1796
1976
  try { layout.closeDetails() } catch (e) {}
1797
1977
  setHeroDetailsWidth(event.currentTarget, 0)
1798
1978
  },
1799
- title: '关闭',
1800
- 'aria-label': '关闭资源管理器',
1979
+ title: T('explorer.close'),
1980
+ 'aria-label': T('explorer.closePanel'),
1801
1981
  }, '✕'),
1802
1982
  ),
1803
1983
  ),
@@ -1809,7 +1989,7 @@ window.__ModuleLoader__.load({
1809
1989
  create('span', { className: 'expl-upload-glyph' }, u.done ? '✓' : (u.down ? '⬇' : '⬆')),
1810
1990
  create('span', { className: 'expl-upload-text' },
1811
1991
  u.done ? u.name
1812
- : (u.down ? '下载中 ' : '上传中 ') + (u.totalFiles > 1 ? '(' + u.idx + '/' + u.totalFiles + ') ' : '') + u.name,
1992
+ : (u.down ? T('explorer.downloading') : T('explorer.uploading')) + (u.totalFiles > 1 ? '(' + u.idx + '/' + u.totalFiles + ') ' : '') + u.name,
1813
1993
  ),
1814
1994
  !u.done && u.total ? create('span', { className: 'expl-upload-pct' }, pct + '%') : null,
1815
1995
  )
@@ -1819,7 +1999,7 @@ window.__ModuleLoader__.load({
1819
1999
  s.error ? create('div', { className: 'expl-error' }, s.error) : null,
1820
2000
  s.notice ? create('div', { className: 'expl-notice' }, s.notice) : null,
1821
2001
  s.selected.size > 1
1822
- ? create('div', { className: 'expl-bulk' }, '已选 ' + s.selected.size + ' 项 · 拖到文件夹可移动')
2002
+ ? create('div', { className: 'expl-bulk' }, T('explorer.bulkSelected', { n: s.selected.size }))
1823
2003
  : null,
1824
2004
  container,
1825
2005
  s.menu ? create('div', { className: 'expl-menu-mask', onMouseDown: function () { setState({ menu: null }) }, onContextMenu: function (e) { e.preventDefault(); setState({ menu: null }) } }) : null,
@@ -1841,8 +2021,8 @@ window.__ModuleLoader__.load({
1841
2021
  type: 'button',
1842
2022
  className: 'expl-toggle expl-tool' + (s.open ? ' expl-active' : ''),
1843
2023
  onClick: function () { try { if (s.open) layout.closeDetails(); else layout.openDetails() } catch (e) {} },
1844
- title: '项目资源管理器',
1845
- 'aria-label': '项目资源管理器',
2024
+ title: T('explorer.label'),
2025
+ 'aria-label': T('explorer.label'),
1846
2026
  }, '📁')
1847
2027
  }
1848
2028
 
@@ -1870,8 +2050,8 @@ window.__ModuleLoader__.load({
1870
2050
  try { layout.openDetails() } catch (e) {}
1871
2051
  }
1872
2052
  },
1873
- title: '项目资源管理器',
1874
- 'aria-label': '项目资源管理器',
2053
+ title: T('explorer.label'),
2054
+ 'aria-label': T('explorer.label'),
1875
2055
  }, '📁'))
1876
2056
  }
1877
2057
 
@@ -1938,7 +2118,7 @@ window.__ModuleLoader__.load({
1938
2118
  el.style.cssText = 'display:inline-flex;align-items:center;margin-left:10px;font-weight:400;font-size:13px;color:var(--dsw-alias-label-caption);'
1939
2119
  el.style.setProperty('color', 'var(--dsw-alias-label-caption)', 'important')
1940
2120
  el.style.setProperty('-webkit-text-fill-color', 'var(--dsw-alias-label-caption)', 'important')
1941
- el.textContent = '正在等待模型' + new Array(dotsRef.current + 1).join('.')
2121
+ el.textContent = T('speed.waiting') + new Array(dotsRef.current + 1).join('.')
1942
2122
  return
1943
2123
  }
1944
2124
  // 流式阶段:token 计数立即显示;瞬时速率窗口未满(tps=0)时先不显示徽标,
@@ -2112,7 +2292,7 @@ window.__ModuleLoader__.load({
2112
2292
  pendingRef.current = false
2113
2293
  pending[1](false)
2114
2294
  if (!result || !result.ok) {
2115
- var err = result && result.error ? (result.error.message + ' (' + result.error.code + ')') : '操作失败'
2295
+ var err = result && result.error ? (result.error.message + ' (' + result.error.code + ')') : T('speed.operateFail')
2116
2296
  actionError[1](err)
2117
2297
  }
2118
2298
  return result
@@ -2230,6 +2410,11 @@ window.__ModuleLoader__.load({
2230
2410
  var sessions = ctx.sessions
2231
2411
  var remoteGoals = ctx.remote.goals
2232
2412
  var disposers = []
2413
+ // i18n:注册双语词典;模块级 T 供组件外回调(菜单/错误横幅)取当前语言文案。
2414
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
2415
+ ctx.effect(function () { return ctx.locale.register(NS, DICT) }, 'dsh-soup: dictionaries')
2416
+ if (typeof ctx.locale.bind === 'function') T = ctx.locale.bind(NS)
2417
+ }
2233
2418
  // 自动刷新心跳:1s 固定心跳 + 到期才探测(基础 3s;宿主不可达时指数退避,封顶 60s)。
2234
2419
  // 面板关且无预览时 tick 内直接返回(零请求);首个 tick 只建基线。
2235
2420
  if (timerRef && typeof timerRef.interval === 'function') {
@@ -2238,7 +2423,7 @@ window.__ModuleLoader__.load({
2238
2423
  }
2239
2424
  slots.inject('conversation.session.header.utilities', function () {
2240
2425
  disposers.push(slots.register(
2241
- { name: 'conversation.session.header.utilities', id: 'dsh-soup-toggle', order: 10, label: '资源管理器' },
2426
+ { name: 'conversation.session.header.utilities', id: 'dsh-soup-toggle', order: 10, locale: NS, label: function () { return T('explorer.label') } },
2242
2427
  function (props) { return create(HeaderAction, props) },
2243
2428
  ))
2244
2429
  })
@@ -2246,7 +2431,7 @@ window.__ModuleLoader__.load({
2246
2431
  disposers.push(slots.register(
2247
2432
  // `details` is a single slot occupied by the shell at priority 0.
2248
2433
  // Lower priorities render first, so this intentionally shadows it.
2249
- { name: 'details', priority: -1 },
2434
+ { name: 'details', priority: -1, locale: NS },
2250
2435
  function (props) { return create(Panel, props) },
2251
2436
  ))
2252
2437
  })
@@ -2254,7 +2439,7 @@ window.__ModuleLoader__.load({
2254
2439
  // 槽位 entry 是静态的,多文件由 FilesView 内部子 tab 条管理。
2255
2440
  slots.inject('conversation.view', function () {
2256
2441
  disposers.push(slots.register(
2257
- { name: 'conversation.view', id: 'dsh-soup-files', order: 20, label: FILES_TAB_LABEL },
2442
+ { name: 'conversation.view', id: 'dsh-soup-files', order: 20, locale: NS, label: function () { return T('files.tab') } },
2258
2443
  function (props) { return create(FilesView, props) },
2259
2444
  ))
2260
2445
  })
@@ -2274,7 +2459,7 @@ window.__ModuleLoader__.load({
2274
2459
  }
2275
2460
  slots.inject('conversation.input.dock', function () {
2276
2461
  disposers.push(slots.register(
2277
- { name: 'conversation.input.dock', id: 'dsh-soup-hero-toggle', order: -10 },
2462
+ { name: 'conversation.input.dock', id: 'dsh-soup-hero-toggle', order: -10, locale: NS },
2278
2463
  function (props) { return create(HeroAction, props) },
2279
2464
  ))
2280
2465
  // GoalBar 多行版:同 id 'goal'、更低 priority (-1) -> 遮蔽默认单行实现。
@@ -2310,7 +2495,7 @@ window.__ModuleLoader__.load({
2310
2495
  function (props) { return create(GoalDock, props) },
2311
2496
  ))
2312
2497
  disposers.push(slots.register(
2313
- { name: 'conversation.input.dock', id: 'dsh-speed-badge', order: 30, label: '速度徽标' },
2498
+ { name: 'conversation.input.dock', id: 'dsh-speed-badge', order: 30, locale: NS, label: function () { return T('speed.label') } },
2314
2499
  function (props) { return create(SpeedBadge, props) },
2315
2500
  ))
2316
2501
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lyhue1991/dsh-soup",
3
3
  "description": "DSH 项目资源管理器:右侧 details 列文件树(并列网格、宽度与左侧工作区一致 264-420px),有 Session log 时在其右侧、空白新会话在输入框上方右端提供 📁 切换按钮;支持多选、双击展开/打开、右键菜单(打开/系统打开/重命名/废纸篓/新建/复制路径)、拖拽移动、上传。文件「打开」在会话区新增「文件」标签页(原生 conversation.view,与对话/轨迹同级):文本可编辑 ⌘S 保存、图片预览、二进制提示。附带模型吞吐徽标:输入框正上方显示「正在等待模型...」与彩色 t/s 徽标(数据来自 llm/stream 真实流)。附带 GoalBar 多行展示:把原生单行截断 GoalBar 放开为多行完整显示,编辑用 textarea,工具与数据仍走原生 goal。",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {