dsh-long-plugins 3.0.0 → 3.0.2

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/client/client.js CHANGED
@@ -13,8 +13,19 @@ window.__ModuleLoader__.load({
13
13
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
14
14
 
15
15
  const React = require('react')
16
+ const API_PATH = '/api/dsh-uploads'
17
+ const DOWNLOAD_PATH = '/api/dsh-uploads/download'
18
+ const PREVIEW_PATH = '/api/dsh-uploads/preview'
16
19
  const HIDDEN_LABEL = '__dsh_upload_hidden__:'
17
20
 
21
+ function downloadUrl(name) {
22
+ return `${DOWNLOAD_PATH}?name=${encodeURIComponent(name)}`
23
+ }
24
+
25
+ function previewUrl(name) {
26
+ return `${PREVIEW_PATH}?name=${encodeURIComponent(name)}`
27
+ }
28
+
18
29
  function errorMessage(error) {
19
30
  return error instanceof Error ? error.message : String(error)
20
31
  }
@@ -617,6 +628,305 @@ window.__ModuleLoader__.load({
617
628
  )
618
629
  }
619
630
 
631
+ function UploadSettingsSection() {
632
+ const [state, setState] = React.useState({
633
+ loading: true,
634
+ root: '',
635
+ maxFileBytes: 0,
636
+ totalMaxBytes: 0,
637
+ usedBytes: 0,
638
+ files: [],
639
+ error: '',
640
+ })
641
+ const [deleting, setDeleting] = React.useState('')
642
+ const [batchBusy, setBatchBusy] = React.useState(false)
643
+ const [selected, setSelected] = React.useState(() => new Set())
644
+ const [preview, setPreview] = React.useState(null)
645
+ const [previewMaximized, setPreviewMaximized] = React.useState(false)
646
+ const [dayFilter, setDayFilter] = React.useState('')
647
+ const [search, setSearch] = React.useState('')
648
+ const [deleteEnabled, setDeleteEnabled] = React.useState(false)
649
+
650
+ async function refresh() {
651
+ setState((current) => ({ ...current, loading: true, error: '' }))
652
+ try {
653
+ const response = await fetch(API_PATH, { cache: 'no-store' })
654
+ const body = await responseJson(response)
655
+ setState({
656
+ loading: false,
657
+ root: body.root,
658
+ maxFileBytes: body.maxFileBytes,
659
+ totalMaxBytes: body.totalMaxBytes,
660
+ usedBytes: body.usedBytes,
661
+ files: body.files,
662
+ error: '',
663
+ })
664
+ } catch (error) {
665
+ setState((current) => ({ ...current, loading: false, error: errorMessage(error) }))
666
+ }
667
+ }
668
+
669
+ React.useEffect(() => {
670
+ refresh()
671
+ }, [])
672
+
673
+ async function remove(name) {
674
+ if (!globalThis.confirm(`确定删除“${name}”吗?此操作不可恢复。`)) return
675
+ setDeleting(name)
676
+ try {
677
+ const response = await fetch(`${API_PATH}?name=${encodeURIComponent(name)}`, { method: 'DELETE' })
678
+ await responseJson(response)
679
+ if (preview !== null && preview.name === name) closePreview()
680
+ await refresh()
681
+ } catch (error) {
682
+ setState((current) => ({ ...current, error: errorMessage(error) }))
683
+ } finally {
684
+ setDeleting('')
685
+ }
686
+ }
687
+
688
+ async function batchDelete() {
689
+ const names = Array.from(selected || [])
690
+ if (names.length === 0) return
691
+ if (!globalThis.confirm(`确定删除所选 ${names.length} 个文件吗?此操作不可恢复。`)) return
692
+ setBatchBusy(true)
693
+ let err = ''
694
+ for (const name of names) {
695
+ try {
696
+ const response = await fetch(`${API_PATH}?name=${encodeURIComponent(name)}`, { method: 'DELETE' })
697
+ if (!response.ok) { const b = await response.json().catch(() => ({})); err = err || (b.error || `HTTP ${response.status}`) }
698
+ } catch (e) { err = err || errorMessage(e) }
699
+ }
700
+ setState((current) => ({ ...current, error: err }))
701
+ setBatchBusy(false)
702
+ setSelected(new Set())
703
+ await refresh()
704
+ }
705
+
706
+ async function previewFile(name) {
707
+ setPreviewMaximized(false)
708
+ try {
709
+ const isOffice = /\.(docx|xlsx|pptx)$/i.test(name)
710
+ if (isOffice) {
711
+ // 先打开弹窗并提示转换中(NAS 上 docx/xlsx 转换可能耗时 1~2 秒)
712
+ setPreview({ name, officeLoading: true })
713
+ const response = await fetch(previewUrl(name), { cache: 'no-store' })
714
+ if (!response.ok) {
715
+ const body = await response.json().catch(() => ({}))
716
+ throw new Error(body.error || `HTTP ${response.status}`)
717
+ }
718
+ const data = await response.json().catch(() => ({}))
719
+ setPreview({
720
+ name,
721
+ officeHtml: data.officeHtml ?? '<p style="font-family:sans-serif;padding:12px">(无法渲染此文档)</p>',
722
+ })
723
+ return
724
+ }
725
+ // 无法内嵌预览的类型(压缩包、程序、视频、字体等):点预览直接下载。
726
+ if (!isInlinePreviewable(name)) {
727
+ triggerDownload(downloadUrl(name), name)
728
+ return
729
+ }
730
+ const response = await fetch(previewUrl(name), { cache: 'no-store' })
731
+ if (!response.ok) {
732
+ const body = await response.json().catch(() => ({}))
733
+ throw new Error(body.error || `HTTP ${response.status}`)
734
+ }
735
+ const type = response.headers.get('content-type') || ''
736
+ if (type.startsWith('image/')) {
737
+ const blob = await response.blob()
738
+ const url = URL.createObjectURL(blob)
739
+ setPreview({ url, name })
740
+ return
741
+ }
742
+ // PDF / txt / 其它可内嵌文件:弹窗内嵌预览(iframe 指向预览端点),
743
+ // 只有用户点「打开」才在新浏览器标签中打开。
744
+ setPreview({ url: previewUrl(name), name })
745
+ } catch (error) {
746
+ setState((current) => ({ ...current, error: errorMessage(error) }))
747
+ }
748
+ }
749
+
750
+ function closePreview() {
751
+ if (preview?.url) URL.revokeObjectURL(preview.url)
752
+ setPreview(null)
753
+ }
754
+
755
+ /** Group files by calendar day of their modifiedAt (YYYY-MM-DD, desc). */
756
+ function groupFilesByDate(files) {
757
+ const groups = []
758
+ const byDay = new Map()
759
+ for (const file of files) {
760
+ let day = ''
761
+ try {
762
+ day = localDay(file.modifiedAt)
763
+ } catch {
764
+ day = '未知日期'
765
+ }
766
+ if (!byDay.has(day)) byDay.set(day, [])
767
+ byDay.get(day).push(file)
768
+ }
769
+ const days = [...byDay.keys()].sort((a, b) => b.localeCompare(a))
770
+ for (const day of days) groups.push({ day, files: byDay.get(day) })
771
+ return groups
772
+ }
773
+ // 日期筛选 + 文件名搜索:先按所选天/关键词过滤 state.files,再始终按天分组(空=全部)。
774
+ const shownFiles = (dayFilter || search)
775
+ ? state.files.filter((f) => (dayFilter ? (() => { try { return localDay(f.modifiedAt) === dayFilter } catch { return false } })() : true) && matchesSearch(f, search))
776
+ : state.files
777
+ const dateGroups = groupFilesByDate(shownFiles)
778
+
779
+ return React.createElement(
780
+ 'section',
781
+ { className: 'dsh-upload-settings' },
782
+ React.createElement(
783
+ 'div',
784
+ { className: 'dsh-upload-settings-head' },
785
+ React.createElement(
786
+ 'div',
787
+ null,
788
+ React.createElement('h2', null, '上传文件'),
789
+ React.createElement('p', null, '管理从输入框上传到 Harness 容器中的文件。'),
790
+ ),
791
+ React.createElement(
792
+ 'div',
793
+ { className: 'dsh-upload-head-actions' },
794
+ React.createElement(SearchPopup, { value: search, onChange: setSearch, placeholder: '搜索文件名…' }),
795
+ React.createElement(DateFilter, { value: dayFilter, onChange: setDayFilter, placeholder: '选择日期' }),
796
+ React.createElement(
797
+ 'button',
798
+ { type: 'button', className: 'dsh-upload-refresh', disabled: state.loading, onClick: refresh },
799
+ state.loading ? '刷新中…' : '刷新',
800
+ ),
801
+ React.createElement(
802
+ 'label',
803
+ { className: 'dsh-upload-del-toggle' },
804
+ React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => { setDeleteEnabled(e.target.checked); if (!e.target.checked) setSelected(new Set()) } }),
805
+ '开启删除',
806
+ ),
807
+ deleteEnabled && React.createElement('label',
808
+ { className: 'dsh-upload-del-toggle' },
809
+ React.createElement('input', { type: 'checkbox', checked: (state.files || []).length > 0 && (selected || new Set()).size === (state.files || []).length, onChange: (e) => { setSelected(e.target.checked ? new Set((state.files || []).map((f) => f.name)) : new Set()) } }),
810
+ '全选',
811
+ ),
812
+ React.createElement(
813
+ 'button',
814
+ { type: 'button', className: 'dsh-upload-batchdel', disabled: !deleteEnabled || (selected || new Set()).size === 0 || batchBusy, onClick: batchDelete },
815
+ batchBusy ? '删除中…' : `批量删除(${(selected || new Set()).size})`,
816
+ ),
817
+ ),
818
+ ),
819
+ React.createElement(
820
+ 'div',
821
+ { className: 'dsh-upload-root' },
822
+ React.createElement('span', null, '固定目录'),
823
+ React.createElement('code', null, state.root || '读取中…'),
824
+ state.maxFileBytes
825
+ ? React.createElement('small', null, `单文件上限 ${sizeText(state.maxFileBytes)}`)
826
+ : null,
827
+ state.totalMaxBytes
828
+ ? React.createElement('small', null, `已使用 ${sizeText(state.usedBytes)} / ${sizeText(state.totalMaxBytes)}`)
829
+ : null,
830
+ ),
831
+ state.error ? React.createElement('div', { className: 'dsh-upload-error' }, state.error) : null,
832
+ !state.loading && state.files.length === 0
833
+ ? React.createElement('div', { className: 'dsh-upload-empty' }, '当前没有已上传文件。')
834
+ : null,
835
+ !state.loading && (dayFilter !== '' || search !== '') && shownFiles.length === 0 && state.files.length > 0
836
+ ? React.createElement('div', { className: 'dsh-upload-empty' }, '没有匹配的文件。')
837
+ : null,
838
+ preview
839
+ ? React.createElement(
840
+ 'div',
841
+ { className: 'dsh-upload-preview-overlay' + (previewMaximized ? ' dsh-upload-preview-overlay-max' : ''), onClick: closePreview },
842
+ React.createElement(
843
+ 'div',
844
+ { className: 'dsh-upload-preview-card' + (previewMaximized ? ' dsh-upload-preview-card-max' : ''), onClick: (event) => event.stopPropagation() },
845
+ React.createElement(
846
+ 'div',
847
+ { className: 'dsh-upload-preview-head' },
848
+ React.createElement('strong', null, preview.name),
849
+ React.createElement(
850
+ 'div',
851
+ { className: 'dsh-upload-preview-actions', style: { display: 'flex', gap: 8, alignItems: 'center' } },
852
+ !preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
853
+ ? React.createElement('a', { href: preview.officeHtml !== void 0 ? downloadUrl(preview.name) : previewUrl(preview.name), target: '_blank', rel: 'noopener noreferrer', className: 'dsh-upload-preview-open' }, '打开')
854
+ : null,
855
+ !preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
856
+ ? React.createElement('a', { href: downloadUrl(preview.name), download: preview.name, className: 'dsh-upload-preview-open' }, '下载')
857
+ : null,
858
+ React.createElement('button', { type: 'button', onClick: () => { const m = !previewMaximized; setPreviewMaximized(m); try { const el = document.querySelector('.dsh-upload-preview-card'); if (m && el && el.requestFullscreen) el.requestFullscreen(); else if (document.fullscreenElement) document.exitFullscreen() } catch (e) {} } }, previewMaximized ? '还原' : '放大'),
859
+ React.createElement('button', { type: 'button', className: 'dsh-upload-preview-del', disabled: deleting === preview.name, onClick: () => remove(preview.name) }, deleting === preview.name ? '删除中…' : '删除'),
860
+ React.createElement('button', { type: 'button', onClick: closePreview }, '关闭'),
861
+ ),
862
+ ),
863
+ preview.url && preview.url.startsWith('blob:')
864
+ ? React.createElement('img', { src: preview.url, alt: preview.name, className: 'dsh-upload-preview-img' })
865
+ : React.createElement(
866
+ 'div',
867
+ { style: { display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0, background: '#fff' } },
868
+ preview.officeLoading === true
869
+ ? React.createElement('div', { className: 'dsh-upload-preview-loading' }, '转换中…')
870
+ : preview.officeHtml !== void 0
871
+ ? React.createElement('iframe', { title: preview.name, srcDoc: preview.officeHtml, style: previewMaximized ? { width: '100%', height: 'calc(100vh - 60px)', border: 'none', background: '#fff', flex: 1 } : { width: '100%', height: '70vh', border: 'none', background: '#fff' } })
872
+ : preview.url && /\.pdf$/i.test(preview.name)
873
+ ? React.createElement('embed', { src: preview.url, type: 'application/pdf', title: preview.name, style: previewMaximized ? { width: '100%', height: 'calc(100vh - 60px)', border: 'none', background: '#fff', flex: 1 } : { width: '100%', height: '70vh', border: 'none', background: '#fff' } })
874
+ : React.createElement('iframe', { title: preview.name, src: preview.url, style: previewMaximized ? { width: '100%', height: 'calc(100vh - 60px)', border: 'none', background: '#fff', flex: 1 } : { width: '100%', height: '70vh', border: 'none', background: '#fff' } }),
875
+ ),
876
+ ),
877
+ )
878
+ : null,
879
+ React.createElement(
880
+ 'div',
881
+ { className: 'dsh-upload-list' },
882
+ (dateGroups === null ? [{ day: null, files: shownFiles }] : dateGroups).map((group) => React.createElement(
883
+ 'div',
884
+ { key: group.day ?? '__all__', className: 'dsh-upload-group' },
885
+ group.day !== null
886
+ ? React.createElement(
887
+ 'div',
888
+ { className: 'dsh-upload-group-day' },
889
+ group.day,
890
+ React.createElement('span', null, `${group.files.length} 个文件`),
891
+ )
892
+ : null,
893
+ group.files.map((file) => React.createElement(
894
+ 'div',
895
+ { className: 'dsh-upload-row', key: file.name },
896
+ deleteEnabled && React.createElement('input', { type: 'checkbox', className: 'dsh-upload-select', checked: (selected || new Set()).has(file.name), onChange: (e) => { const s = new Set(selected || []); if (e.target.checked) s.add(file.name); else s.delete(file.name); setSelected(s) } }),
897
+ React.createElement(
898
+ 'span',
899
+ { className: 'dsh-upload-file-name', title: file.path },
900
+ React.createElement('span', { className: 'dsh-upload-file-label' }, file.name.replace(/\.[^.]+$/, '')),
901
+ React.createElement('span', { className: 'dsh-upload-file-type' }, fileTypeLabel(file.name)),
902
+ ),
903
+ React.createElement('span', { className: 'dsh-upload-file-meta' }, `${sizeText(file.size)} · ${dateText(file.modifiedAt)}`),
904
+ React.createElement(
905
+ 'div',
906
+ { className: 'dsh-upload-actions' },
907
+ React.createElement(
908
+ 'button',
909
+ { type: 'button', className: 'dsh-upload-copy', onClick: () => {
910
+ const s = String(file.path || file.name)
911
+ const base = state.root ? String(state.root).replace(/\/[^/]*$/, '') : ''
912
+ copyText(base && s.indexOf(base) === 0 ? s.slice(base.length).replace(/^\/+/, '') : s)
913
+ } },
914
+ '复制路径',
915
+ ),
916
+ React.createElement(
917
+ 'button',
918
+ { type: 'button', disabled: !deleteEnabled || deleting === file.name, onClick: () => remove(file.name) },
919
+ deleting === file.name ? '删除中…' : '删除',
920
+ ),
921
+ React.createElement('a', { href: downloadUrl(file.name), download: file.name }, '下载'),
922
+ React.createElement('button', { type: 'button', className: 'dsh-upload-preview', onClick: () => previewFile(file.name) }, '预览'),
923
+ ),
924
+ )),
925
+ )),
926
+ ),
927
+ )
928
+ }
929
+
620
930
  function apply(ctx) {
621
931
  const mod = window.__dshLongMod || (() => true)
622
932
  ctx.effect(() => {
@@ -627,6 +937,15 @@ window.__ModuleLoader__.load({
627
937
  document.head.appendChild(style)
628
938
  return () => style.remove()
629
939
  }, 'dsh-long-plugins: uploads+workspace styles')
940
+ // 上传文件预览/管理设置区:由 uploadPreview 控制
941
+ if (mod('uploadPreview')) {
942
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
943
+ name: 'settings.section',
944
+ id: 'uploaded-files',
945
+ order: 30,
946
+ label: '上传文件',
947
+ }, UploadSettingsSection))
948
+ }
630
949
  // 输出文件(工作区)设置区:由 workspace 控制
631
950
  if (mod('workspace')) {
632
951
  ctx.slots.inject('settings.section', () => ctx.slots.register({
@@ -2297,6 +2616,7 @@ window.__ModuleLoader__.load({
2297
2616
  .dsh-glass-note{margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}
2298
2617
  `
2299
2618
  const DSH_LONG_MODULES = [
2619
+ ['uploadPreview', '上传文件预览/管理'],
2300
2620
  ['skillDocs', '技能管理'],
2301
2621
  ['balance', '账户余额'],
2302
2622
  ['sessionCost', '会话成本'],
@@ -2374,9 +2694,25 @@ window.__ModuleLoader__.load({
2374
2694
 
2375
2695
  // 模块开关缓存:页面加载时同步读取(让 main apply 的 modEnabled 门控生效);
2376
2696
  // 配置加载(applyTheme)/保存(dsh-long 保存)后更新。开关在下次刷新生效。
2697
+ // 模块开关:以服务端为准(启动时同步拉取,避免 localStorage 缓存过期导致模块被误关)
2377
2698
  try {
2378
- const _m = localStorage.getItem('dsh-long:modules')
2379
- if (_m) window.__dshLongModules = JSON.parse(_m)
2699
+ const _x = new XMLHttpRequest()
2700
+ _x.open('GET', '/api/dsh-uploads/modules-config', false)
2701
+ _x.send()
2702
+ if (_x.status === 200) {
2703
+ const _b = JSON.parse(_x.responseText)
2704
+ if (_b && _b.cfg && _b.cfg.modules && typeof _b.cfg.modules === 'object') {
2705
+ window.__dshLongModules = _b.cfg.modules
2706
+ try { localStorage.setItem('dsh-long:modules', JSON.stringify(_b.cfg.modules)) } catch (e) {}
2707
+ }
2708
+ }
2709
+ } catch (_) {}
2710
+ // 回退:服务端不可用时用 localStorage 缓存
2711
+ try {
2712
+ if (!window.__dshLongModules) {
2713
+ const _m = localStorage.getItem('dsh-long:modules')
2714
+ if (_m) window.__dshLongModules = JSON.parse(_m)
2715
+ }
2380
2716
  } catch (_) {}
2381
2717
  // 全局模块开关查询(供各组件/apply 内部判断)
2382
2718
  window.__dshLongMod = (n) => (window.__dshLongModules ? window.__dshLongModules[n] !== false : true)
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-long-plugins",
3
3
  "description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, Markdown-to-Word (md2docx), and polished file preview (Word & PowerPoint real preview with zoom/pan, rendered Markdown), plus an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Markdown 转 Word(md2docx)、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。",
4
- "version": "3.0.0",
4
+ "version": "3.0.2",
5
5
  "entry": {
6
6
  "name": "dsh-long-plugins",
7
7
  "inject": [
package/lib/index.js CHANGED
@@ -1649,7 +1649,7 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1649
1649
  };
1650
1650
 
1651
1651
  // 各模块开关(dsh-long 设置区):读写 ~/.dsh-long-plugins/modules.json(独立于 dsh-span 的 RA-Span 配置)。
1652
- const MODULES_DEFAULTS = { skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: false, sessionCost: false };
1652
+ const MODULES_DEFAULTS = { uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: false, sessionCost: false };
1653
1653
  const modulesFile = join(homedir(), ".dsh-long-plugins", "modules.json");
1654
1654
  async function readModulesJSON() {
1655
1655
  try {
@@ -2477,6 +2477,12 @@ export async function apply(ctx, config = {}) {
2477
2477
  if (!isTrustedUploadRequest(req, trustedHosts)) throw new HttpError(403, "forbidden");
2478
2478
  };
2479
2479
 
2480
+ ctx.effect(() => ctx.webServer.register({
2481
+ kind: "exact",
2482
+ path: API_PATH,
2483
+ handler: handlers.api,
2484
+ }), "dsh-long-plugins: upload/list/delete route");
2485
+
2480
2486
  ctx.effect(() => ctx.webServer.register({
2481
2487
  kind: "exact",
2482
2488
  path: DOWNLOAD_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-long-plugins",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
4
  "description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, and polished file preview (Word & PowerPoint real preview with zoom/pan, rendered Markdown), plus an auto-repair install for DSH core patches (reverse-proxy WebSocket heartbeat, upgrade relink). | 一个插件整合 DSH Web 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。Office 读取/生成已独立到 dsh-office-reader;RA-Span 已独立到 dsh-span。",
5
5
  "type": "module",
6
6
  "license": "MIT",