dsh-long-plugins 2.4.1 → 2.4.5
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/README.md +3 -54
- package/client/client.js +102 -41
- package/dsh.plugin.json +1 -1
- package/lib/index.js +102 -9
- package/package.json +1 -1
- package/skill/dsh-upgrade/SKILL.md +5 -1
package/README.md
CHANGED
|
@@ -122,60 +122,9 @@ MIT
|
|
|
122
122
|
|
|
123
123
|
完整的版权声明与来源见 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
📄 更新记录见 [`CHANGELOG.md`](CHANGELOG.md)。
|
|
126
126
|
|
|
127
|
-
###
|
|
127
|
+
### 中间/提取产物 → `.outputdir`(工作区列表自动隐藏)
|
|
128
|
+
读取、提取、解析等生成的**中间产物**(`_sd_extract`、`weiquan_review` 之类)请统一写到 **`.outputdir`**(或 `_` 开头的目录)。插件在工作区/输出文件列表中**自动隐藏 `.` / `_` 开头的目录及其下层内容**,所以**无论产物目录叫什么名字**,只要落到 `.outputdir`(或 `_` 前缀)就不会显示;真正要展示的**文档产物**放在普通目录即可正常显示。
|
|
128
129
|
|
|
129
|
-
修复输入框实底、浅色主题可读性问题,并为输入框、背景罩新增浅色/深色主题的分套设置。
|
|
130
|
-
|
|
131
|
-
🐛 Bug 修复
|
|
132
|
-
|
|
133
|
-
- 输入框实底跟随主题:此前用操作系统深色偏好判断,导致浅色主题下黑底黑字。改为读取 DSH 真实主题(`<html>` 上的 `color-scheme`),浅色 = 白底深字、深色 = 深底浅字,切换主题自动实时重算。
|
|
134
|
-
- 浅色主题可读性:统一桌面浅色下,气泡、工具调用名(Bash 等)、推理(think)标签、底部状态/统计行文字在背景图上过浅看不清。已按主题提高对比度并加描边;状态/统计行改用稳定的 `data-slot` 槽位定位(不再依赖会变的混淆类名),提高升级稳定性。
|
|
135
|
-
|
|
136
|
-
✨ 新增功能
|
|
137
|
-
|
|
138
|
-
- 输入框设置(设置面板新增"输入框"区):浅色主题、深色主题可分别设置背景颜色与不透明度,支持一键"恢复原生"。
|
|
139
|
-
- 背景罩设置(设置面板新增"背景罩"区):浅色主题、深色主题可分别设置罩色与罩强度,切换主题自动应用对应的一套。
|
|
140
|
-
|
|
141
|
-
🎨 视觉 / 交互
|
|
142
|
-
|
|
143
|
-
- 左栏「新会话」按钮去掉白/黑实底边框,文字改为跟随侧边栏的白字+描边,更好融入背景。
|
|
144
|
-
|
|
145
|
-
### v2.2.0
|
|
146
|
-
|
|
147
|
-
界面优化与重构
|
|
148
|
-
|
|
149
|
-
- 背景图独立层:背景图改为独立图层,新增「背景图模糊 / 背景图透明度」滑杆(单独一组,不影响磨砂卡),并新增「背景罩层颜色」色盘,背景图可模糊、可调透明度、罩色可选。
|
|
150
|
-
- 会话区:去掉会话区独立磨砂卡片,直接透出共享背景(背景图 + 罩层颜色 + 模糊),版面更通透。
|
|
151
|
-
- 输入区:整个输入区外围(composer 根/位置座)透明透出背景。
|
|
152
|
-
- 可读性:浅色主题下弱化标签/元信息文字加深,对比度更高。
|
|
153
|
-
|
|
154
|
-
### v2.1.0
|
|
155
|
-
- 增强:毛玻璃界面(玻璃拟态)进一步打磨。
|
|
156
|
-
- 主题感知磨砂:磨砂卡片颜色跟随系统深色/浅色主题自动反转(浅色=白磨砂深字 / 深色=深磨砂浅字)。
|
|
157
|
-
- 会话区内容反底色全透明化:去掉代码块/提示/气泡等反底色小块,统一融入磨砂卡片。
|
|
158
|
-
- 会话区「启用玻璃」开关:可单独禁用会话区磨砂(还原原生)。
|
|
159
|
-
- 会话区/输入区各自独立「罩色 + 罩强度 + 透明度」调节。
|
|
160
|
-
- 新增「背景图罩」:背景图上叠一层半透明罩色,深度可调。
|
|
161
|
-
- 手机端/窄屏(≤768px)自动禁用毛玻璃,还原 DSH 原生。
|
|
162
|
-
- 输入卡片/底部白色层透明化,更透背景。
|
|
163
|
-
- 修复:输入框下拉/悬浮菜单(`/` 命令、`@` 提及)背景透明,改为不透明随主题色。
|
|
164
|
-
- 修复:玻璃界面自定义背景图上传超大文件报 `Unexpected token '<'` 错误——上传上限提升到 4MiB。
|
|
165
|
-
|
|
166
|
-
### v2.0.0
|
|
167
|
-
- 新增:毛玻璃界面(玻璃拟态)。为 DSH 网页叠加自定义毛玻璃效果,将界面打磨成"玻璃"质感。
|
|
168
|
-
- **自定义背景图**:上传一张背景图铺满整个页面(含左边栏、顶部、会话区),实现桌面壁纸式的背景;左边栏 / 顶部半透明透出背景图,界面更通透。
|
|
169
|
-
- **分区独立磨砂与罩色**:会话区、输入区各自独立设置「罩色 + 罩强度」,互不影响,实现"玻璃卡片"的分区质感;会话窗口磨砂玻璃悬浮卡片效果。
|
|
170
|
-
- **磨砂模糊**:全局磨砂模糊强度可调(0–80px),文字清晰可读。
|
|
171
|
-
- **跟随系统主题**:界面文字/内容颜色跟随系统深色/浅色主题,不强制改色,保证任意主题下可读。
|
|
172
|
-
- **设置面板「毛玻璃界面」**:总开关、背景图上传、磨砂模糊、会话区/输入区各自的罩色与罩强度,实时保存生效。
|
|
173
|
-
|
|
174
|
-
### v1.3.10
|
|
175
|
-
- 新增 `.xlsx` 浏览器端预览:用 SheetJS 解析并显示为表格(行列标头、合并单元格、多工作表切换),预览支持 − / + / 适合宽度缩放,默认整表适配;浏览器端处理,不占用 NAS 本地资源。
|
|
176
|
-
- 工作区浏览、工作区文件、上传文件列表中的 `.xlsx` 均接入该预览页。
|
|
177
|
-
- `.csv` / `.tsv` 保留表格预览(首行作表头)。
|
|
178
|
-
- 文件列表按钮顺序调整:工作区浏览页为 复制路径、重命名、下载、预览;上传/输出文件面板为 复制路径、删除、下载、预览。
|
|
179
|
-
- 「上传文件」「输出文件」新增「开启删除」开关:关闭时删除按钮置灰禁用,开启后可删除;上传文件的「复制路径」不再使用删除红色。
|
|
180
|
-
- 手机端(≤640px)文件列表顶部操作行保持单行:搜索/日期等宽、刷新/开关靠右。
|
|
181
130
|
|
package/client/client.js
CHANGED
|
@@ -683,23 +683,16 @@ window.__ModuleLoader__.load({
|
|
|
683
683
|
/** 🔍 放大镜搜索:点击图标弹出搜索框,输入即过滤;✕ 清除并关闭。不自动隐藏(需人工关闭或刷新)。 */
|
|
684
684
|
function SearchPopup({ value, onChange, placeholder }) {
|
|
685
685
|
const [open, setOpen] = React.useState(false)
|
|
686
|
-
const [pos, setPos] = React.useState({ top: 72, left: 8 })
|
|
687
686
|
const ref = React.useRef(null)
|
|
688
687
|
React.useEffect(() => { if (open && ref.current) { try { ref.current.focus() } catch (e) {} } }, [open])
|
|
689
|
-
const toggle = (
|
|
690
|
-
if (!open && e && e.currentTarget) {
|
|
691
|
-
const r = e.currentTarget.getBoundingClientRect()
|
|
692
|
-
setPos({ top: r.bottom + 8, left: Math.max(8, Math.min(r.left, (window.innerWidth || 0) - 280)) })
|
|
693
|
-
}
|
|
694
|
-
setOpen((o) => !o)
|
|
695
|
-
}
|
|
688
|
+
const toggle = () => setOpen((o) => !o)
|
|
696
689
|
return React.createElement('div', { className: 'dsh-searchpop' },
|
|
697
690
|
React.createElement('button', {
|
|
698
691
|
type: 'button', className: 'dsh-searchpop-btn',
|
|
699
692
|
onClick: toggle,
|
|
700
693
|
title: '搜索文件名', 'aria-label': '搜索文件名',
|
|
701
694
|
}, '🔍'),
|
|
702
|
-
open && React.createElement('div', { className: 'dsh-searchpop-box'
|
|
695
|
+
open && React.createElement('div', { className: 'dsh-searchpop-box' },
|
|
703
696
|
React.createElement('input', {
|
|
704
697
|
ref, type: 'text', className: 'dsh-searchpop-input', value,
|
|
705
698
|
onChange: (e) => onChange(e.target.value), placeholder: placeholder || '搜索文件名…', autoFocus: true,
|
|
@@ -724,6 +717,8 @@ window.__ModuleLoader__.load({
|
|
|
724
717
|
error: '',
|
|
725
718
|
})
|
|
726
719
|
const [deleting, setDeleting] = React.useState('')
|
|
720
|
+
const [batchBusy, setBatchBusy] = React.useState(false)
|
|
721
|
+
const [selected, setSelected] = React.useState(() => new Set())
|
|
727
722
|
const [preview, setPreview] = React.useState(null)
|
|
728
723
|
const [previewMaximized, setPreviewMaximized] = React.useState(false)
|
|
729
724
|
const [dayFilter, setDayFilter] = React.useState('')
|
|
@@ -768,6 +763,24 @@ window.__ModuleLoader__.load({
|
|
|
768
763
|
}
|
|
769
764
|
}
|
|
770
765
|
|
|
766
|
+
async function batchDelete() {
|
|
767
|
+
const names = Array.from(selected || [])
|
|
768
|
+
if (names.length === 0) return
|
|
769
|
+
if (!globalThis.confirm(`确定删除所选 ${names.length} 个文件吗?此操作不可恢复。`)) return
|
|
770
|
+
setBatchBusy(true)
|
|
771
|
+
let err = ''
|
|
772
|
+
for (const name of names) {
|
|
773
|
+
try {
|
|
774
|
+
const response = await fetch(`${API_PATH}?name=${encodeURIComponent(name)}`, { method: 'DELETE' })
|
|
775
|
+
if (!response.ok) { const b = await response.json().catch(() => ({})); err = err || (b.error || `HTTP ${response.status}`) }
|
|
776
|
+
} catch (e) { err = err || errorMessage(e) }
|
|
777
|
+
}
|
|
778
|
+
setState((current) => ({ ...current, error: err }))
|
|
779
|
+
setBatchBusy(false)
|
|
780
|
+
setSelected(new Set())
|
|
781
|
+
await refresh()
|
|
782
|
+
}
|
|
783
|
+
|
|
771
784
|
async function previewFile(name) {
|
|
772
785
|
setPreviewMaximized(false)
|
|
773
786
|
try {
|
|
@@ -866,9 +879,19 @@ window.__ModuleLoader__.load({
|
|
|
866
879
|
React.createElement(
|
|
867
880
|
'label',
|
|
868
881
|
{ className: 'dsh-upload-del-toggle' },
|
|
869
|
-
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => setDeleteEnabled(e.target.checked) }),
|
|
882
|
+
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => { setDeleteEnabled(e.target.checked); if (!e.target.checked) setSelected(new Set()) } }),
|
|
870
883
|
'开启删除',
|
|
871
884
|
),
|
|
885
|
+
deleteEnabled && React.createElement('label',
|
|
886
|
+
{ className: 'dsh-upload-del-toggle' },
|
|
887
|
+
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()) } }),
|
|
888
|
+
'全选',
|
|
889
|
+
),
|
|
890
|
+
React.createElement(
|
|
891
|
+
'button',
|
|
892
|
+
{ type: 'button', className: 'dsh-upload-batchdel', disabled: !deleteEnabled || (selected || new Set()).size === 0 || batchBusy, onClick: batchDelete },
|
|
893
|
+
batchBusy ? '删除中…' : `批量删除(${(selected || new Set()).size})`,
|
|
894
|
+
),
|
|
872
895
|
),
|
|
873
896
|
),
|
|
874
897
|
React.createElement(
|
|
@@ -910,7 +933,7 @@ window.__ModuleLoader__.load({
|
|
|
910
933
|
!preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
|
|
911
934
|
? React.createElement('a', { href: downloadUrl(preview.name), download: preview.name, className: 'dsh-upload-preview-open' }, '下载')
|
|
912
935
|
: null,
|
|
913
|
-
React.createElement('button', { type: 'button', onClick: () => setPreviewMaximized(
|
|
936
|
+
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 ? '还原' : '放大'),
|
|
914
937
|
React.createElement('button', { type: 'button', className: 'dsh-upload-preview-del', disabled: deleting === preview.name, onClick: () => remove(preview.name) }, deleting === preview.name ? '删除中…' : '删除'),
|
|
915
938
|
React.createElement('button', { type: 'button', onClick: closePreview }, '关闭'),
|
|
916
939
|
),
|
|
@@ -948,6 +971,7 @@ window.__ModuleLoader__.load({
|
|
|
948
971
|
group.files.map((file) => React.createElement(
|
|
949
972
|
'div',
|
|
950
973
|
{ className: 'dsh-upload-row', key: file.name },
|
|
974
|
+
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) } }),
|
|
951
975
|
React.createElement(
|
|
952
976
|
'span',
|
|
953
977
|
{ className: 'dsh-upload-file-name', title: file.path },
|
|
@@ -1007,7 +1031,7 @@ window.__ModuleLoader__.load({
|
|
|
1007
1031
|
.dsh-searchpop{position:relative;display:inline-flex;align-items:center;flex:none}
|
|
1008
1032
|
.dsh-searchpop-btn{width:30px;height:30px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:14px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}
|
|
1009
1033
|
.dsh-searchpop-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
|
|
1010
|
-
.dsh-searchpop-box{position:
|
|
1034
|
+
.dsh-searchpop-box{position:absolute;top:calc(100% + 6px);left:0;z-index:60;display:flex;align-items:center;gap:6px;background:var(--dsw-specific-input-major,#0f1720);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:6px;box-shadow:var(--dsw-shadow-lv3);max-width:calc(100vw - 16px)}
|
|
1011
1035
|
.dsh-searchpop-input{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);padding:5px 9px;font:inherit;font-size:12px;line-height:18px;min-width:160px}
|
|
1012
1036
|
.dsh-searchpop-input::placeholder{color:var(--dsw-alias-label-tertiary)}
|
|
1013
1037
|
.dsh-searchpop-clear{background:transparent;border:none;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1;cursor:pointer;padding:0 3px}
|
|
@@ -1045,9 +1069,13 @@ window.__ModuleLoader__.load({
|
|
|
1045
1069
|
.dsh-upload-actions button:disabled{opacity:.5;cursor:not-allowed;color:var(--dsw-alias-label-tertiary)}
|
|
1046
1070
|
.dsh-upload-del-toggle{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;user-select:none}
|
|
1047
1071
|
.dsh-upload-del-toggle input{accent-color:var(--dsw-alias-state-business-primary);cursor:pointer}
|
|
1072
|
+
.dsh-upload-batchdel{display:inline-flex;align-items:center;height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font-size:12px;line-height:1;cursor:pointer;white-space:nowrap}
|
|
1073
|
+
.dsh-upload-batchdel:disabled{opacity:.5;cursor:not-allowed}
|
|
1074
|
+
.dsh-upload-batchdel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
|
|
1075
|
+
.dsh-upload-select{flex:none;width:15px;height:15px;margin:0 6px 0 0;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer}
|
|
1048
1076
|
.dsh-ws-del:disabled{opacity:.5;cursor:not-allowed;color:var(--dsw-alias-label-tertiary)!important}
|
|
1049
1077
|
.dsh-upload-actions a,.dsh-upload-actions button{white-space:nowrap}
|
|
1050
|
-
@media (max-width:640px){.dsh-upload-row{align-items:stretch;flex-direction:column;gap:4px}.dsh-upload-file-name{white-space:normal;word-break:break-all}.dsh-upload-actions{width:100%;display:flex;gap:6px}.dsh-upload-actions a,.dsh-upload-actions button{flex:1;text-align:center;white-space:nowrap;padding:6px 4px}.dsh-upload-chip{min-width:160px}.dsh-upload-settings-head{flex-direction:column;align-items:stretch;gap:10px}.dsh-upload-head-actions{width:100%;flex-wrap:
|
|
1078
|
+
@media (max-width:640px){.dsh-upload-row{align-items:stretch;flex-direction:column;gap:4px}.dsh-upload-file-name{white-space:normal;word-break:break-all}.dsh-upload-actions{width:100%;display:flex;gap:6px}.dsh-upload-actions a,.dsh-upload-actions button{flex:1;text-align:center;white-space:nowrap;padding:6px 4px}.dsh-upload-chip{min-width:160px}.dsh-upload-settings-head{flex-direction:column;align-items:stretch;gap:10px}.dsh-upload-head-actions{width:100%;flex-wrap:wrap;justify-content:flex-start;gap:6px}.dsh-upload-head-actions .dsh-searchpop,.dsh-upload-head-actions .dsh-datefilter{flex:none;min-width:0}.dsh-upload-head-actions .dsh-upload-refresh{flex:none;text-align:center;padding:7px 8px;max-width:none}.dsh-upload-head-actions .dsh-upload-del-toggle,.dsh-upload-head-actions .dsh-upload-batchdel{flex:none;white-space:nowrap;padding:0 4px}}
|
|
1051
1079
|
.dsh-ws-folder:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1052
1080
|
.dsh-ws-file-type{flex:none;font-size:10px;line-height:14px;padding:1px 6px;border-radius:5px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);white-space:nowrap}
|
|
1053
1081
|
.dsh-upload-preview-del{color:var(--dsw-alias-state-error-primary)!important}
|
|
@@ -1091,6 +1119,8 @@ window.__ModuleLoader__.load({
|
|
|
1091
1119
|
const [dayFilter, setDayFilter] = React.useState('')
|
|
1092
1120
|
const [search, setSearch] = React.useState('')
|
|
1093
1121
|
const [deleteEnabled, setDeleteEnabled] = React.useState(false)
|
|
1122
|
+
const [batchBusy, setBatchBusy] = React.useState(false)
|
|
1123
|
+
const [selected, setSelected] = React.useState(() => new Set())
|
|
1094
1124
|
const [copied, setCopied] = React.useState(false)
|
|
1095
1125
|
const [editing, setEditing] = React.useState(false)
|
|
1096
1126
|
const [edited, setEdited] = React.useState('')
|
|
@@ -1286,6 +1316,28 @@ window.__ModuleLoader__.load({
|
|
|
1286
1316
|
.filter((g) => g.files.length > 0)
|
|
1287
1317
|
: groups
|
|
1288
1318
|
|
|
1319
|
+
// 批量删除当前勾选的文件(受「开启删除」开关控制)
|
|
1320
|
+
const batchDelete = async () => {
|
|
1321
|
+
const paths = Array.from(selected || [])
|
|
1322
|
+
if (paths.length === 0) return
|
|
1323
|
+
if (!window.confirm(`确定删除所选 ${paths.length} 个文件吗?此操作不可恢复。`)) return
|
|
1324
|
+
setBatchBusy(true)
|
|
1325
|
+
let err = null
|
|
1326
|
+
for (const path of paths) {
|
|
1327
|
+
try {
|
|
1328
|
+
const res = await fetch('/api/dsh-uploads/workspace-file/delete', {
|
|
1329
|
+
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path }),
|
|
1330
|
+
})
|
|
1331
|
+
const data = await res.json().catch(() => ({}))
|
|
1332
|
+
if (!(data.ok === true)) err = err || (data.error || `HTTP ${res.status}`)
|
|
1333
|
+
} catch (e) { err = err || String((e && e.message) || e) }
|
|
1334
|
+
}
|
|
1335
|
+
setError(err)
|
|
1336
|
+
setBatchBusy(false)
|
|
1337
|
+
setSelected(new Set())
|
|
1338
|
+
load()
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1289
1341
|
return React.createElement(
|
|
1290
1342
|
'div',
|
|
1291
1343
|
{ style: { display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 } },
|
|
@@ -1294,9 +1346,16 @@ window.__ModuleLoader__.load({
|
|
|
1294
1346
|
React.createElement(SearchPopup, { value: search, onChange: setSearch, placeholder: '搜索文件名…' }),
|
|
1295
1347
|
React.createElement(DateFilter, { value: dayFilter, onChange: setDayFilter, placeholder: '选择日期' }),
|
|
1296
1348
|
React.createElement('label', { className: 'dsh-upload-del-toggle' },
|
|
1297
|
-
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => setDeleteEnabled(e.target.checked) }),
|
|
1349
|
+
React.createElement('input', { type: 'checkbox', checked: deleteEnabled, onChange: (e) => { setDeleteEnabled(e.target.checked); if (!e.target.checked) setSelected(new Set()) } }),
|
|
1298
1350
|
'开启删除',
|
|
1299
1351
|
),
|
|
1352
|
+
deleteEnabled && React.createElement('label', { className: 'dsh-upload-del-toggle' },
|
|
1353
|
+
React.createElement('input', { type: 'checkbox', checked: (shownGroups || []).reduce((n, g) => n + g.files.length, 0) > 0 && (selected || new Set()).size === (shownGroups || []).reduce((n, g) => n + g.files.length, 0), onChange: (e) => { setSelected(e.target.checked ? new Set((shownGroups || []).flatMap((g) => g.files.map((f) => f.path))) : new Set()) } }),
|
|
1354
|
+
'全选',
|
|
1355
|
+
),
|
|
1356
|
+
React.createElement('button', { type: 'button', className: 'dsh-upload-batchdel', disabled: !deleteEnabled || (selected || new Set()).size === 0 || batchBusy, onClick: batchDelete },
|
|
1357
|
+
batchBusy ? '删除中…' : `批量删除(${(selected || new Set()).size})`,
|
|
1358
|
+
),
|
|
1300
1359
|
),
|
|
1301
1360
|
error !== null && React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, error),
|
|
1302
1361
|
groups === null && error === null && React.createElement('div', { style: metaStyle }, '加载中…'),
|
|
@@ -1319,6 +1378,7 @@ window.__ModuleLoader__.load({
|
|
|
1319
1378
|
!collapsed[group.folder] && group.files.map((f) => React.createElement(
|
|
1320
1379
|
'div',
|
|
1321
1380
|
{ key: f.path, className: 'dsh-ws-row', style: rowStyle },
|
|
1381
|
+
deleteEnabled && React.createElement('input', { type: 'checkbox', className: 'dsh-upload-select', checked: (selected || new Set()).has(f.path), onChange: (e) => { const s = new Set(selected || []); if (e.target.checked) s.add(f.path); else s.delete(f.path); setSelected(s) } }),
|
|
1322
1382
|
React.createElement(
|
|
1323
1383
|
'span',
|
|
1324
1384
|
{ className: 'dsh-ws-name', style: { ...nameStyle, display: 'flex', alignItems: 'center', gap: 6 }, title: f.path },
|
|
@@ -1352,7 +1412,7 @@ window.__ModuleLoader__.load({
|
|
|
1352
1412
|
(preview.url !== void 0 || preview.officeHtml !== void 0 || preview.mdHtml !== void 0) && preview.loading !== true && React.createElement('a', { href: '/api/dsh-uploads/workspace-file?path=' + encodeURIComponent(preview.path) + '&download=1', download: preview.name, style: { ...btnStyle, color: 'var(--dsw-alias-state-business-primary)' } }, '下载'),
|
|
1353
1413
|
(preview.url !== void 0 || preview.officeHtml !== void 0 || preview.mdHtml !== void 0) && preview.loading !== true && React.createElement('a', { href: preview.url !== void 0 ? preview.url : '/api/dsh-uploads/workspace-preview?path=' + encodeURIComponent(preview.path), target: '_blank', rel: 'noopener noreferrer', style: { ...btnStyle, color: 'var(--dsw-alias-state-business-primary)' } }, '打开'),
|
|
1354
1414
|
React.createElement('button', { type: 'button', style: delStyle, disabled: busy, onClick: () => doDelete(preview.path) }, '删除'),
|
|
1355
|
-
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => setMaximized(
|
|
1415
|
+
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => { const m = !maximized; setMaximized(m); try { const el = document.querySelector('.dsh-ws-preview-card'); if (m && el && el.requestFullscreen) el.requestFullscreen(); else if (document.fullscreenElement) document.exitFullscreen() } catch (e) {} } }, maximized ? '还原' : '放大'),
|
|
1356
1416
|
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => setPreview(null) }, '关闭'),
|
|
1357
1417
|
),
|
|
1358
1418
|
React.createElement(
|
|
@@ -1462,8 +1522,8 @@ window.__ModuleLoader__.load({
|
|
|
1462
1522
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
1463
1523
|
/** `skillDocs` namespace dictionaries. */
|
|
1464
1524
|
const zh = {
|
|
1465
|
-
"nav": "
|
|
1466
|
-
"files.hint": "
|
|
1525
|
+
"nav": "技能管理",
|
|
1526
|
+
"files.hint": "技能管理目录(可折叠,点击预览可编辑)",
|
|
1467
1527
|
"files.loading": "加载中…",
|
|
1468
1528
|
"files.empty": "目录为空",
|
|
1469
1529
|
"files.preview": "预览",
|
|
@@ -1509,19 +1569,12 @@ window.__ModuleLoader__.load({
|
|
|
1509
1569
|
/** 🔍 放大镜搜索弹窗(技能文档用):点击弹出搜索框,✕ 清除并关闭。不自动隐藏。 */
|
|
1510
1570
|
function SkillSearchPop({ value, onChange, placeholder }) {
|
|
1511
1571
|
const [open, setOpen] = react.useState(false);
|
|
1512
|
-
const [pos, setPos] = react.useState({ top: 72, left: 8 });
|
|
1513
1572
|
const ref = react.useRef(null);
|
|
1514
1573
|
react.useEffect(() => { if (open && ref.current) { try { ref.current.focus(); } catch (e) {} } }, [open]);
|
|
1515
|
-
const toggle = (
|
|
1516
|
-
if (!open && e && e.currentTarget) {
|
|
1517
|
-
const r = e.currentTarget.getBoundingClientRect();
|
|
1518
|
-
setPos({ top: r.bottom + 8, left: Math.max(8, Math.min(r.left, (window.innerWidth || 0) - 280)) });
|
|
1519
|
-
}
|
|
1520
|
-
setOpen((o) => !o);
|
|
1521
|
-
};
|
|
1574
|
+
const toggle = () => setOpen((o) => !o);
|
|
1522
1575
|
return react_jsx_runtime.jsxs("div", { className: "dsh-searchpop", children: [
|
|
1523
1576
|
react_jsx_runtime.jsx("button", { type: "button", className: "dsh-searchpop-btn", onClick: toggle, title: "搜索技能/文件", "aria-label": "搜索技能/文件", children: "🔍" }),
|
|
1524
|
-
open && react_jsx_runtime.jsxs("div", { className: "dsh-searchpop-box",
|
|
1577
|
+
open && react_jsx_runtime.jsxs("div", { className: "dsh-searchpop-box", children: [
|
|
1525
1578
|
react_jsx_runtime.jsx("input", { ref, type: "text", className: "dsh-searchpop-input", value, onChange: (e) => onChange(e.target.value), placeholder: placeholder || "搜索技能/文件…", autoFocus: true }),
|
|
1526
1579
|
value !== "" && react_jsx_runtime.jsx("button", { type: "button", className: "dsh-searchpop-clear", title: "清除", "aria-label": "清除搜索", onMouseDown: (e) => { e.preventDefault(); e.stopPropagation(); }, onClick: (e) => { e.preventDefault(); e.stopPropagation(); onChange(""); setOpen(false); }, children: "✕" })
|
|
1527
1580
|
] })
|
|
@@ -1539,34 +1592,37 @@ window.__ModuleLoader__.load({
|
|
|
1539
1592
|
const [busy, setBusy] = react.useState(false);
|
|
1540
1593
|
const [savedFlash, setSavedFlash] = react.useState(false);
|
|
1541
1594
|
const [copiedFlash, setCopiedFlash] = react.useState(false);
|
|
1595
|
+
const [root, setRoot] = react.useState("global");
|
|
1542
1596
|
const toggleFolder = (folder) => setCollapsed((prev) => ({ ...prev, [folder]: !prev[folder] }));
|
|
1543
1597
|
|
|
1544
1598
|
const load = react.useCallback(async () => {
|
|
1545
1599
|
try {
|
|
1546
|
-
const
|
|
1600
|
+
const cwd = (window.__dshCurrentCwd || "").replace(/\\/g, "/");
|
|
1601
|
+
const ws = cwd ? cwd.split("/").filter(Boolean).pop() : "";
|
|
1602
|
+
const res = await fetch("/dsh-skill-docs/skill-docs?root=" + encodeURIComponent(root) + "&ws=" + encodeURIComponent(ws), { headers: { Accept: "application/json" } });
|
|
1547
1603
|
if (!res.ok) { setError(`HTTP ${res.status}`); return; }
|
|
1548
1604
|
const data = await res.json();
|
|
1549
1605
|
if (data.ok === true) { setGroups(data.groups); setError(null); }
|
|
1550
1606
|
else setError(data.error);
|
|
1551
1607
|
}
|
|
1552
1608
|
catch (e) { setError(String((e && e.message) || e)); }
|
|
1553
|
-
}, []);
|
|
1609
|
+
}, [root]);
|
|
1554
1610
|
react.useEffect(() => { load(); }, [load]);
|
|
1555
1611
|
|
|
1556
|
-
const openDoc = async (path) => {
|
|
1557
|
-
setPreview({ path, loading: true });
|
|
1612
|
+
const openDoc = async (path, ws = "") => {
|
|
1613
|
+
setPreview({ path, loading: true, ws });
|
|
1558
1614
|
setEditing(false);
|
|
1559
1615
|
setMaximized(false);
|
|
1560
1616
|
try {
|
|
1561
|
-
const res = await fetch("/dsh-skill-docs/skill-doc?path=" + encodeURIComponent(path), { headers: { Accept: "application/json" } });
|
|
1617
|
+
const res = await fetch("/dsh-skill-docs/skill-doc?root=" + encodeURIComponent(root) + "&ws=" + encodeURIComponent(ws) + "&path=" + encodeURIComponent(path), { headers: { Accept: "application/json" } });
|
|
1562
1618
|
const data = await res.json();
|
|
1563
1619
|
if (data.ok === true) {
|
|
1564
|
-
setPreview(data);
|
|
1620
|
+
setPreview({ ...data, ws });
|
|
1565
1621
|
setEdited(data.content !== void 0 ? data.content : "");
|
|
1566
1622
|
}
|
|
1567
|
-
else setPreview({ path, error: data.error });
|
|
1623
|
+
else setPreview({ path, error: data.error, ws });
|
|
1568
1624
|
}
|
|
1569
|
-
catch (e) { setPreview({ path, error: String((e && e.message) || e) }); }
|
|
1625
|
+
catch (e) { setPreview({ path, error: String((e && e.message) || e), ws }); }
|
|
1570
1626
|
};
|
|
1571
1627
|
|
|
1572
1628
|
const doSave = async () => {
|
|
@@ -1576,7 +1632,7 @@ window.__ModuleLoader__.load({
|
|
|
1576
1632
|
const res = await fetch("/dsh-skill-docs/skill-doc/save", {
|
|
1577
1633
|
method: "POST",
|
|
1578
1634
|
headers: { "content-type": "application/json" },
|
|
1579
|
-
body: JSON.stringify({ path: preview.path, content: edited })
|
|
1635
|
+
body: JSON.stringify({ path: preview.path, content: edited, root, ws: preview.ws || "" })
|
|
1580
1636
|
});
|
|
1581
1637
|
const data = await res.json().catch(() => ({}));
|
|
1582
1638
|
if (data.ok === true) {
|
|
@@ -1667,6 +1723,10 @@ window.__ModuleLoader__.load({
|
|
|
1667
1723
|
return react_jsx_runtime.jsxs("div", {
|
|
1668
1724
|
style: { display: "flex", flexDirection: "column", gap: 6, minWidth: 0 },
|
|
1669
1725
|
children: [
|
|
1726
|
+
react_jsx_runtime.jsxs("div", { style: { display: "flex", gap: 6, flexWrap: "wrap" }, children: [
|
|
1727
|
+
react_jsx_runtime.jsx("button", { type: "button", style: { ...btnStyle, fontWeight: root === "global" ? 700 : 400 }, onClick: () => setRoot("global"), children: "全局技能" }),
|
|
1728
|
+
react_jsx_runtime.jsx("button", { type: "button", style: { ...btnStyle, fontWeight: root === "workspace" ? 700 : 400 }, onClick: () => setRoot("workspace"), children: "工作区技能" })
|
|
1729
|
+
] }),
|
|
1670
1730
|
react_jsx_runtime.jsx("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", minWidth: 0 }, children: [
|
|
1671
1731
|
react_jsx_runtime.jsx("span", { style: { fontSize: 13, color: "var(--dsw-alias-label-tertiary)" }, children: t("files.hint") }),
|
|
1672
1732
|
react_jsx_runtime.jsx(SkillSearchPop, { value: search, onChange: setSearch, placeholder: "搜索技能/文件…" })
|
|
@@ -1691,19 +1751,20 @@ window.__ModuleLoader__.load({
|
|
|
1691
1751
|
children: [
|
|
1692
1752
|
react_jsx_runtime.jsx("span", { style: nameStyle, title: f.path, children: f.path }),
|
|
1693
1753
|
react_jsx_runtime.jsx("span", { style: metaStyle, children: `${f.size < 1024 ? `${f.size} B` : `${(f.size / 1024).toFixed(1)} KiB`}` }),
|
|
1694
|
-
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: () => openDoc(f.path), children: t("files.preview") }),
|
|
1754
|
+
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: () => openDoc(f.path, group.folder), children: t("files.preview") }),
|
|
1695
1755
|
react_jsx_runtime.jsx("a", { href: "/dsh-skill-docs/skill-doc?path=" + encodeURIComponent(f.path) + "&download=1", download: f.name, style: btnStyle, children: t("files.download") })
|
|
1696
1756
|
]
|
|
1697
1757
|
}, f.path))
|
|
1698
1758
|
]
|
|
1699
1759
|
}, group.folder);
|
|
1700
1760
|
}),
|
|
1701
|
-
searching && shownGroups !== null && shownGroups.length === 0 && react_jsx_runtime.jsx("div", { style: metaStyle, children: "
|
|
1761
|
+
searching && shownGroups !== null && shownGroups.length === 0 && react_jsx_runtime.jsx("div", { style: metaStyle, children: "没有匹配的技能管理" }),
|
|
1702
1762
|
preview !== null && react_jsx_runtime.jsxs("div", {
|
|
1703
1763
|
style: overlayStyle,
|
|
1704
1764
|
onClick: () => setPreview(null),
|
|
1705
1765
|
children: [
|
|
1706
1766
|
react_jsx_runtime.jsxs("div", {
|
|
1767
|
+
className: "dsh-skill-preview-card",
|
|
1707
1768
|
style: cardStyle,
|
|
1708
1769
|
onClick: (e) => e.stopPropagation(),
|
|
1709
1770
|
children: [
|
|
@@ -1714,7 +1775,7 @@ window.__ModuleLoader__.load({
|
|
|
1714
1775
|
preview.binary !== true && react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, disabled: busy, onClick: () => { if (editing) { setEdited(preview.content !== void 0 ? preview.content : ""); setEditing(false); } else setEditing(true); }, children: editing ? t("files.cancelEdit") : t("files.edit") }),
|
|
1715
1776
|
editing && react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, disabled: busy, onClick: doSave, children: savedFlash ? t("files.saved") : t("files.save") }),
|
|
1716
1777
|
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: copyAll, children: copiedFlash ? t("files.copied") : t("files.copy") }),
|
|
1717
|
-
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: () => setMaximized(
|
|
1778
|
+
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: () => { const m = !maximized; setMaximized(m); try { const el = document.querySelector(".dsh-skill-preview-card"); if (m && el && el.requestFullscreen) el.requestFullscreen(); else if (document.fullscreenElement) document.exitFullscreen() } catch (e) {} }, children: maximized ? t("files.restore") : t("files.maximize") }),
|
|
1718
1779
|
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: () => setPreview(null), children: t("files.close") })
|
|
1719
1780
|
]
|
|
1720
1781
|
}),
|
|
@@ -1805,7 +1866,7 @@ window.__ModuleLoader__.load({
|
|
|
1805
1866
|
"files.binary": "二进制文件,无法预览,请下载后查看",
|
|
1806
1867
|
"files.truncated": "(内容过大,仅显示前 256KB)",
|
|
1807
1868
|
"files.close": "关闭预览",
|
|
1808
|
-
"skills.hint": "
|
|
1869
|
+
"skills.hint": "技能管理(各技能的 SKILL.md),可预览",
|
|
1809
1870
|
"balance": "余额",
|
|
1810
1871
|
"spend": "本会话约"
|
|
1811
1872
|
};
|
|
@@ -2387,7 +2448,7 @@ window.__ModuleLoader__.load({
|
|
|
2387
2448
|
}
|
|
2388
2449
|
const WorkspaceFilesButton = ({ sessionId, useSessions }) => {
|
|
2389
2450
|
const cur = useSessions((s) => (sessionId === void 0 ? void 0 : s.byId[sessionId]?.cwd))
|
|
2390
|
-
React.useEffect(() => { if (cur) lastCwd = normPath(cur) }, [cur])
|
|
2451
|
+
React.useEffect(() => { if (cur) { lastCwd = normPath(cur); try { window.__dshCurrentCwd = normPath(cur) } catch (e) {} } }, [cur])
|
|
2391
2452
|
// Windows 下 cwd 是反斜杠路径(C:\...\jacky),按 / 与 \ 都能切,取末段文件夹名。
|
|
2392
2453
|
const ws = cur ? normPath(cur).split('/').filter(Boolean).pop() : ''
|
|
2393
2454
|
return React.createElement(
|
|
@@ -3389,7 +3450,7 @@ window.__ModuleLoader__.load({
|
|
|
3389
3450
|
['uploadDragDrop', '附件拖放上传'],
|
|
3390
3451
|
['uploadPaste', '附件粘贴上传'],
|
|
3391
3452
|
['uploadPreview', '上传文件预览/管理'],
|
|
3392
|
-
['skillDocs', '
|
|
3453
|
+
['skillDocs', '技能管理'],
|
|
3393
3454
|
['balance', '账户余额'],
|
|
3394
3455
|
['workspace', '输出文件预览/管理'],
|
|
3395
3456
|
['mobile', '移动端布局'],
|
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": "2.4.
|
|
4
|
+
"version": "2.4.5",
|
|
5
5
|
"entry": {
|
|
6
6
|
"name": "dsh-long-plugins",
|
|
7
7
|
"inject": [
|
package/lib/index.js
CHANGED
|
@@ -385,6 +385,8 @@ async function collectGroups(root, excluded, hiddenNames = []) {
|
|
|
385
385
|
const groups = [];
|
|
386
386
|
for (const entry of entries) {
|
|
387
387
|
if (!entry.isDirectory() || entry.name === excluded) continue;
|
|
388
|
+
// 排除点开头(.dsh/.outputdir)与下划线开头(_sd_extract 等中间产物)的文件夹及其下层所有内容
|
|
389
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
388
390
|
const files = [];
|
|
389
391
|
const walk = async (dir) => {
|
|
390
392
|
let sub;
|
|
@@ -397,6 +399,8 @@ async function collectGroups(root, excluded, hiddenNames = []) {
|
|
|
397
399
|
sub.sort((a, b) => a.name.localeCompare(b.name));
|
|
398
400
|
for (const item of sub) {
|
|
399
401
|
const abs = join(dir, item.name);
|
|
402
|
+
// 嵌套里的点/下划线开头条目(夹/文件)也一并排除
|
|
403
|
+
if (item.name.startsWith(".") || item.name.startsWith("_")) continue;
|
|
400
404
|
if (item.isDirectory()) {
|
|
401
405
|
await walk(abs);
|
|
402
406
|
} else if (item.isFile()) {
|
|
@@ -427,6 +431,47 @@ async function collectGroups(root, excluded, hiddenNames = []) {
|
|
|
427
431
|
return groups;
|
|
428
432
|
}
|
|
429
433
|
|
|
434
|
+
/** 扫描 workspace 根下的子目录:凡有 .dsh/skills 就收集其技能文件,按工作区名分组;无文件则该组 files 为空。 */
|
|
435
|
+
async function collectWorkspaceSkills(baseRoot) {
|
|
436
|
+
let entries;
|
|
437
|
+
try {
|
|
438
|
+
entries = await readdir(baseRoot, { withFileTypes: true });
|
|
439
|
+
} catch {
|
|
440
|
+
return [];
|
|
441
|
+
}
|
|
442
|
+
const groups = [];
|
|
443
|
+
for (const e of entries) {
|
|
444
|
+
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
445
|
+
const skillsDir = resolve(baseRoot, e.name, ".dsh", "skills");
|
|
446
|
+
try {
|
|
447
|
+
const st = await stat(skillsDir);
|
|
448
|
+
if (!st.isDirectory()) continue;
|
|
449
|
+
} catch { continue; }
|
|
450
|
+
const files = [];
|
|
451
|
+
const walk = async (dir) => {
|
|
452
|
+
let sub;
|
|
453
|
+
try { sub = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
454
|
+
sub.sort((a, b) => a.name.localeCompare(b.name));
|
|
455
|
+
for (const item of sub) {
|
|
456
|
+
if (item.name.startsWith(".")) continue;
|
|
457
|
+
const abs = join(dir, item.name);
|
|
458
|
+
try {
|
|
459
|
+
if (item.isDirectory()) await walk(abs);
|
|
460
|
+
else if (item.isFile()) {
|
|
461
|
+
const info = await stat(abs);
|
|
462
|
+
files.push({ path: relative(skillsDir, abs).split(sep).join("/"), name: item.name, size: info.size, mtime: info.mtimeMs });
|
|
463
|
+
}
|
|
464
|
+
} catch {}
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
await walk(skillsDir);
|
|
468
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
469
|
+
groups.push({ folder: e.name, files });
|
|
470
|
+
}
|
|
471
|
+
groups.sort((a, b) => a.folder.localeCompare(b.folder));
|
|
472
|
+
return groups;
|
|
473
|
+
}
|
|
474
|
+
|
|
430
475
|
export async function sweepUploadTemps(root) {
|
|
431
476
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
432
477
|
const entries = await readdir(root, { withFileTypes: true });
|
|
@@ -1069,6 +1114,8 @@ export function createHandlers(options = {}) {
|
|
|
1069
1114
|
const size = info.size;
|
|
1070
1115
|
const downloadHref = `workspace-file?path=${encodeURIComponent(rel)}&download=1`;
|
|
1071
1116
|
let body;
|
|
1117
|
+
let editable = false;
|
|
1118
|
+
let rawText = "";
|
|
1072
1119
|
if (OFFICE_EXTS.has(ext)) {
|
|
1073
1120
|
const html = await officePreviewHtml(name, buffer);
|
|
1074
1121
|
body = html ? `<div class="office">${html}</div>` : `<p class="unsupported">该 Office 文档无法渲染,请下载查看。</p>`;
|
|
@@ -1081,17 +1128,20 @@ export function createHandlers(options = {}) {
|
|
|
1081
1128
|
// 避免 base64 data URI 在 iframe 内被 Chrome 拒绝。
|
|
1082
1129
|
body = `<iframe class="pdf" src="workspace-file?path=${encodeURIComponent(rel)}&inline=1"></iframe>`;
|
|
1083
1130
|
} else if (/\.(txt|log)$/i.test(name)) {
|
|
1084
|
-
|
|
1131
|
+
editable = true; rawText = buffer.toString("utf8");
|
|
1132
|
+
body = `<pre class="text">${escapeHtml(rawText)}</pre>`;
|
|
1085
1133
|
} else if (/\.(md|markdown)$/i.test(name)) {
|
|
1086
1134
|
// Markdown → 直接渲染成 HTML(真实效果),失败回退源码文本。
|
|
1087
|
-
|
|
1135
|
+
editable = true; rawText = buffer.toString("utf8");
|
|
1136
|
+
body = `<article class="md">${markdownToHtml(rawText)}</article>`;
|
|
1088
1137
|
} else if (/\.(json|ya?ml|py|js|mjs|cjs|ts|sh|css|html?|xml|csv|ini|conf|env|toml|sql|rs|go|c|h|cpp|java|kt|swift|rb|php|vue|jsx|tsx)$/i.test(name)) {
|
|
1089
|
-
|
|
1138
|
+
editable = true; rawText = buffer.toString("utf8");
|
|
1139
|
+
body = `<pre class="text">${escapeHtml(rawText)}</pre>`;
|
|
1090
1140
|
} else {
|
|
1091
1141
|
body = `<p class="unsupported">该文件类型暂不支持预览,请点击右上角「下载」。</p>`;
|
|
1092
1142
|
}
|
|
1093
1143
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
1094
|
-
res.end(previewPageHtml(name, rel, size, downloadHref, body, `workspace-file?path=${encodeURIComponent(rel)}&inline=1
|
|
1144
|
+
res.end(previewPageHtml(name, rel, size, downloadHref, body, `workspace-file?path=${encodeURIComponent(rel)}&inline=1`, editable, rawText));
|
|
1095
1145
|
} catch (error) {
|
|
1096
1146
|
sendError(res, error, onError);
|
|
1097
1147
|
}
|
|
@@ -2443,7 +2493,7 @@ function markdownToHtml(md) {
|
|
|
2443
2493
|
|
|
2444
2494
|
|
|
2445
2495
|
/** 工作区文件预览页面骨架(自包含,无外部依赖)。 */
|
|
2446
|
-
function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "") {
|
|
2496
|
+
function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "", editable = false, rawText = "") {
|
|
2447
2497
|
return `<!DOCTYPE html>
|
|
2448
2498
|
<html lang="zh-CN">
|
|
2449
2499
|
<head>
|
|
@@ -2477,6 +2527,8 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "") {
|
|
|
2477
2527
|
.hint { position:fixed; left:50%; bottom:24px; transform:translateX(-50%); background:#7c2d12; color:#fdba74; border-radius:8px; padding:10px 18px; font-size:13px; display:none; z-index:9; }
|
|
2478
2528
|
.content { padding:20px; max-width:960px; margin:0 auto; }
|
|
2479
2529
|
.text { background:var(--lp-text-bg); border:1px solid var(--lp-border); border-radius:10px; padding:16px; overflow:auto; font-size:13px; line-height:1.7; white-space:pre-wrap; word-break:break-word; }
|
|
2530
|
+
.editor { display:none; width:100%; min-height:60vh; background:var(--lp-text-bg); color:var(--lp-fg); border:1px solid var(--lp-border); border-radius:10px; padding:12px 14px; font:13px/1.7 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; resize:vertical; white-space:pre; word-break:normal; }
|
|
2531
|
+
body.maximized .editor { min-height:calc(100vh - 53px); }
|
|
2480
2532
|
.md { line-height:1.8; font-size:14px; word-break:break-word; }
|
|
2481
2533
|
.md h1,.md h2,.md h3,.md h4,.md h5,.md h6 { line-height:1.45; margin:1.7em 0 .8em; }
|
|
2482
2534
|
.md h1 { font-size:1.5em; }
|
|
@@ -2523,14 +2575,36 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "") {
|
|
|
2523
2575
|
<span class="name">${escapeHtml(name)}</span>
|
|
2524
2576
|
<span class="meta">${escapeHtml(rel)} · ${(size / 1024).toFixed(1)} KB</span>
|
|
2525
2577
|
<span class="spacer"></span>
|
|
2578
|
+
${editable ? `<button class="btn2" type="button" id="editBtn" onclick="editMode()">${ICON_FOLDER} 编辑</button>
|
|
2579
|
+
<button class="btn" type="button" id="saveBtn" onclick="saveFile()" style="display:none">${ICON_DL} 保存</button>` : ""}
|
|
2526
2580
|
${inlineHref ? `<a class="btn" href="${escapeHtml(inlineHref)}" target="_blank" rel="noopener noreferrer">${ICON_EYE} 打开</a>` : ""}
|
|
2527
2581
|
<a class="btn" href="${escapeHtml(downloadHref)}" download>${ICON_DL} 下载</a>
|
|
2528
2582
|
<button class="btn2" type="button" id="maxBtn" onclick="toggleMax()">${ICON_FOLDER} 放大</button>
|
|
2529
2583
|
<button class="btn2" type="button" onclick="closePreview()">${ICON_X} 关闭</button>
|
|
2530
2584
|
</div>
|
|
2531
2585
|
<div class="content">${body}</div>
|
|
2586
|
+
${editable ? `<textarea id="editor" class="editor">${escapeHtml(rawText)}</textarea>` : ""}
|
|
2532
2587
|
<div class="hint" id="closeHint">浏览器不允许脚本直接关闭此标签页,请手动关闭本标签页(或按 Ctrl+W / ⌘+W)。</div>
|
|
2533
2588
|
<script>
|
|
2589
|
+
var SAVE_PATH = ${JSON.stringify("/api/dsh-uploads/workspace-file/save")};
|
|
2590
|
+
var SAVE_REL = ${JSON.stringify(rel)};
|
|
2591
|
+
function editMode() {
|
|
2592
|
+
var editing = document.getElementById('editor').style.display === 'none';
|
|
2593
|
+
document.querySelector('.content').style.display = editing ? 'none' : '';
|
|
2594
|
+
document.getElementById('editor').style.display = editing ? 'block' : 'none';
|
|
2595
|
+
document.getElementById('saveBtn').style.display = editing ? '' : 'none';
|
|
2596
|
+
document.getElementById('editBtn').textContent = editing ? '取消' : '编辑';
|
|
2597
|
+
if (editing) { var ed = document.getElementById('editor'); ed.focus(); }
|
|
2598
|
+
}
|
|
2599
|
+
function saveFile() {
|
|
2600
|
+
var content = document.getElementById('editor').value;
|
|
2601
|
+
var btn = document.getElementById('saveBtn');
|
|
2602
|
+
btn.disabled = true; btn.textContent = '保存中…';
|
|
2603
|
+
fetch(SAVE_PATH, { method:'POST', headers:{'content-type':'application/json'}, body: JSON.stringify({ path: SAVE_REL, content: content }) })
|
|
2604
|
+
.then(function (r) { return r.json().catch(function(){ return {}; }).then(function(b){ return { ok: r.ok, b: b }; }); })
|
|
2605
|
+
.then(function (o) { btn.textContent = o.ok ? '已保存' : ('失败: ' + (o.b && o.b.error || '')); btn.disabled = false; setTimeout(function(){ btn.textContent = '保存'; }, 1400); })
|
|
2606
|
+
.catch(function () { btn.textContent = '网络错误'; btn.disabled = false; setTimeout(function(){ btn.textContent = '保存'; }, 1500); });
|
|
2607
|
+
}
|
|
2534
2608
|
function toggleMax() {
|
|
2535
2609
|
var max = document.body.classList.toggle('maximized');
|
|
2536
2610
|
document.getElementById('maxBtn').textContent = max ? '还原' : '放大';
|
|
@@ -2565,6 +2639,7 @@ function closePreview() {
|
|
|
2565
2639
|
export async function apply(ctx, config = {}) {
|
|
2566
2640
|
const trustedHosts = Array.isArray(config.trustedHosts) ? [...config.trustedHosts] : [];
|
|
2567
2641
|
const skillsRoot = resolve(config.skillsRoot ?? DEFAULT_SKILLS_ROOT);
|
|
2642
|
+
const workspaceRoot = resolveWorkspaceRoot(); // 供 skill-docs 路由"工作区技能"使用
|
|
2568
2643
|
const onError = (error) => ctx.logger.error(error instanceof Error ? error : new Error(String(error)));
|
|
2569
2644
|
|
|
2570
2645
|
const handlers = createHandlers({ trustedHosts, onError, excludedWorkspaceNames: config.excludedWorkspaceNames });
|
|
@@ -2779,6 +2854,19 @@ export async function apply(ctx, config = {}) {
|
|
|
2779
2854
|
}), "dsh-long-plugins: workspace rename route");
|
|
2780
2855
|
|
|
2781
2856
|
// ---- 技能文档 (skill docs) routes ----
|
|
2857
|
+
const skillDocsList = async (req, res) => {
|
|
2858
|
+
const rootParam = (() => { try { return new URL(req.url || "/", "http://dsh.internal").searchParams.get("root") || "global"; } catch { return "global"; } })();
|
|
2859
|
+
const wsParam = (() => { try { return new URL(req.url || "/", "http://dsh.internal").searchParams.get("ws") || ""; } catch { return ""; } })();
|
|
2860
|
+
if (rootParam === "workspace") {
|
|
2861
|
+
// 扫描 workspace 根下的子目录:凡有 .dsh/skills 就识别,按工作区名分组;无文件则该组 files 为空
|
|
2862
|
+
const groups = await collectWorkspaceSkills(workspaceRoot);
|
|
2863
|
+
sendJson(res, 200, { ok: true, root: workspaceRoot, groups });
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const groups = await collectGroups(skillsRoot, "__dsh_none__");
|
|
2867
|
+
sendJson(res, 200, { ok: true, root: skillsRoot, groups });
|
|
2868
|
+
};
|
|
2869
|
+
|
|
2782
2870
|
ctx.effect(() => ctx.webServer.register({
|
|
2783
2871
|
kind: "exact",
|
|
2784
2872
|
path: "/dsh-skill-docs/skill-docs",
|
|
@@ -2789,8 +2877,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2789
2877
|
}
|
|
2790
2878
|
try {
|
|
2791
2879
|
requireTrusted(req);
|
|
2792
|
-
|
|
2793
|
-
sendJson(res, 200, { ok: true, root: skillsRoot, groups });
|
|
2880
|
+
await skillDocsList(req, res);
|
|
2794
2881
|
} catch (error) {
|
|
2795
2882
|
sendError(res, error, onError);
|
|
2796
2883
|
}
|
|
@@ -2813,7 +2900,10 @@ export async function apply(ctx, config = {}) {
|
|
|
2813
2900
|
}
|
|
2814
2901
|
try {
|
|
2815
2902
|
requireTrusted(req);
|
|
2816
|
-
const
|
|
2903
|
+
const rootParam = (() => { try { return new URL(req.url || "/", "http://dsh.internal").searchParams.get("root") || "global"; } catch { return "global"; } })();
|
|
2904
|
+
const wsParam = (() => { try { return new URL(req.url || "/", "http://dsh.internal").searchParams.get("ws") || ""; } catch { return ""; } })();
|
|
2905
|
+
const root = rootParam === "workspace" ? resolve(workspaceRoot, wsParam, ".dsh", "skills") : skillsRoot;
|
|
2906
|
+
const full = safeResolve(root, rel);
|
|
2817
2907
|
if (full === undefined) throw new HttpError(400, "bad path");
|
|
2818
2908
|
const info = await stat(full);
|
|
2819
2909
|
if (!info.isFile()) throw new HttpError(400, "not a file");
|
|
@@ -2865,8 +2955,11 @@ export async function apply(ctx, config = {}) {
|
|
|
2865
2955
|
const body = await readJsonBody(req);
|
|
2866
2956
|
const rel = typeof body === "object" && body !== null ? body.path : undefined;
|
|
2867
2957
|
const content = typeof body === "object" && body !== null ? body.content : undefined;
|
|
2958
|
+
const rootParam = typeof body === "object" && body !== null && body.root === "workspace" ? "workspace" : "global";
|
|
2959
|
+
const wsParam = typeof body === "object" && body !== null && typeof body.ws === "string" ? body.ws : "";
|
|
2960
|
+
const root = rootParam === "workspace" ? resolve(workspaceRoot, wsParam, ".dsh", "skills") : skillsRoot;
|
|
2868
2961
|
if (typeof content !== "string") throw new HttpError(400, "content required");
|
|
2869
|
-
const full = safeResolve(
|
|
2962
|
+
const full = safeResolve(root, rel);
|
|
2870
2963
|
if (full === undefined) throw new HttpError(400, "bad path");
|
|
2871
2964
|
const info = await stat(full);
|
|
2872
2965
|
if (!info.isFile()) throw new HttpError(400, "not a file");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-long-plugins",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
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, 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 心跳、升级重连)。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -84,8 +84,12 @@ fi
|
|
|
84
84
|
2. 设置页各 tab 可进、不再 403/未暴露命名空间(重启后,用浏览器访问,非 raw 127.0.0.1)。
|
|
85
85
|
3. 反代域名访问正常。
|
|
86
86
|
4. 提问/审批窗口:弹出一个问题,等 60s+ 不再自己消失(心跳补丁生效)。
|
|
87
|
-
5.
|
|
87
|
+
5. 插件设置面板「上传文件」「输出文件」「技能文档」「dsh-long」「RA-Span」都在。
|
|
88
88
|
6. `grep -q WEBSOCKET_HEARTBEAT_MS <核心解压路径>/dsh-client-connection/lib/index.js` → 心跳在位。
|
|
89
|
+
7. 「dsh-long」区有各模块开关 +「补丁状态(只读)」;禁用某模块后对应功能不加载(下次刷新生效)。
|
|
90
|
+
8. 工作区「输出文件」/「上传文件」里:点开头文件夹(如 `.dsh`/`.outputdir`)及其下层文件不显示。
|
|
91
|
+
9. 设置面板「技能管理」「上传文件」「输出文件」的 🔍 搜索框:点击后弹出框紧贴按钮下方、滚动不脱离。
|
|
92
|
+
10. **「技能管理」区(全局技能 / 工作区技能两 tab)**:全局技能列 `<DSH_HOME>/skills`;工作区技能扫描工作区根下各子目录的 `.dsh/skills`(无 `.dsh/skills` 则该工作区组为空)。若该区不出现/工作区技能为空,可能 DSH `settings.section` 槽或工作区结构变化——复核。
|
|
89
93
|
|
|
90
94
|
## 硬性安全边界(必须遵守)
|
|
91
95
|
- **不主动 push / 打 tag / 发 Release**;需要发布版本时停下用 ask_user_question 等确认。
|