dsh-long-plugins 2.4.2 → 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 +4 -0
- package/client/client.js +99 -31
- package/dsh.plugin.json +1 -1
- package/lib/index.js +102 -13
- package/package.json +1 -1
- package/skill/dsh-upgrade/SKILL.md +5 -1
package/README.md
CHANGED
|
@@ -124,3 +124,7 @@ MIT
|
|
|
124
124
|
|
|
125
125
|
📄 更新记录见 [`CHANGELOG.md`](CHANGELOG.md)。
|
|
126
126
|
|
|
127
|
+
### 中间/提取产物 → `.outputdir`(工作区列表自动隐藏)
|
|
128
|
+
读取、提取、解析等生成的**中间产物**(`_sd_extract`、`weiquan_review` 之类)请统一写到 **`.outputdir`**(或 `_` 开头的目录)。插件在工作区/输出文件列表中**自动隐藏 `.` / `_` 开头的目录及其下层内容**,所以**无论产物目录叫什么名字**,只要落到 `.outputdir`(或 `_` 前缀)就不会显示;真正要展示的**文档产物**放在普通目录即可正常显示。
|
|
129
|
+
|
|
130
|
+
|
package/client/client.js
CHANGED
|
@@ -717,6 +717,8 @@ window.__ModuleLoader__.load({
|
|
|
717
717
|
error: '',
|
|
718
718
|
})
|
|
719
719
|
const [deleting, setDeleting] = React.useState('')
|
|
720
|
+
const [batchBusy, setBatchBusy] = React.useState(false)
|
|
721
|
+
const [selected, setSelected] = React.useState(() => new Set())
|
|
720
722
|
const [preview, setPreview] = React.useState(null)
|
|
721
723
|
const [previewMaximized, setPreviewMaximized] = React.useState(false)
|
|
722
724
|
const [dayFilter, setDayFilter] = React.useState('')
|
|
@@ -761,6 +763,24 @@ window.__ModuleLoader__.load({
|
|
|
761
763
|
}
|
|
762
764
|
}
|
|
763
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
|
+
|
|
764
784
|
async function previewFile(name) {
|
|
765
785
|
setPreviewMaximized(false)
|
|
766
786
|
try {
|
|
@@ -859,9 +879,19 @@ window.__ModuleLoader__.load({
|
|
|
859
879
|
React.createElement(
|
|
860
880
|
'label',
|
|
861
881
|
{ className: 'dsh-upload-del-toggle' },
|
|
862
|
-
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()) } }),
|
|
863
883
|
'开启删除',
|
|
864
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
|
+
),
|
|
865
895
|
),
|
|
866
896
|
),
|
|
867
897
|
React.createElement(
|
|
@@ -903,7 +933,7 @@ window.__ModuleLoader__.load({
|
|
|
903
933
|
!preview.officeLoading && !(preview.url && preview.url.startsWith('blob:')) && preview.name !== void 0
|
|
904
934
|
? React.createElement('a', { href: downloadUrl(preview.name), download: preview.name, className: 'dsh-upload-preview-open' }, '下载')
|
|
905
935
|
: null,
|
|
906
|
-
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 ? '还原' : '放大'),
|
|
907
937
|
React.createElement('button', { type: 'button', className: 'dsh-upload-preview-del', disabled: deleting === preview.name, onClick: () => remove(preview.name) }, deleting === preview.name ? '删除中…' : '删除'),
|
|
908
938
|
React.createElement('button', { type: 'button', onClick: closePreview }, '关闭'),
|
|
909
939
|
),
|
|
@@ -941,6 +971,7 @@ window.__ModuleLoader__.load({
|
|
|
941
971
|
group.files.map((file) => React.createElement(
|
|
942
972
|
'div',
|
|
943
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) } }),
|
|
944
975
|
React.createElement(
|
|
945
976
|
'span',
|
|
946
977
|
{ className: 'dsh-upload-file-name', title: file.path },
|
|
@@ -1038,9 +1069,13 @@ window.__ModuleLoader__.load({
|
|
|
1038
1069
|
.dsh-upload-actions button:disabled{opacity:.5;cursor:not-allowed;color:var(--dsw-alias-label-tertiary)}
|
|
1039
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}
|
|
1040
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}
|
|
1041
1076
|
.dsh-ws-del:disabled{opacity:.5;cursor:not-allowed;color:var(--dsw-alias-label-tertiary)!important}
|
|
1042
1077
|
.dsh-upload-actions a,.dsh-upload-actions button{white-space:nowrap}
|
|
1043
|
-
@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}}
|
|
1044
1079
|
.dsh-ws-folder:hover{background:var(--dsw-alias-interactive-bg-hover)}
|
|
1045
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}
|
|
1046
1081
|
.dsh-upload-preview-del{color:var(--dsw-alias-state-error-primary)!important}
|
|
@@ -1084,6 +1119,8 @@ window.__ModuleLoader__.load({
|
|
|
1084
1119
|
const [dayFilter, setDayFilter] = React.useState('')
|
|
1085
1120
|
const [search, setSearch] = React.useState('')
|
|
1086
1121
|
const [deleteEnabled, setDeleteEnabled] = React.useState(false)
|
|
1122
|
+
const [batchBusy, setBatchBusy] = React.useState(false)
|
|
1123
|
+
const [selected, setSelected] = React.useState(() => new Set())
|
|
1087
1124
|
const [copied, setCopied] = React.useState(false)
|
|
1088
1125
|
const [editing, setEditing] = React.useState(false)
|
|
1089
1126
|
const [edited, setEdited] = React.useState('')
|
|
@@ -1279,6 +1316,28 @@ window.__ModuleLoader__.load({
|
|
|
1279
1316
|
.filter((g) => g.files.length > 0)
|
|
1280
1317
|
: groups
|
|
1281
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
|
+
|
|
1282
1341
|
return React.createElement(
|
|
1283
1342
|
'div',
|
|
1284
1343
|
{ style: { display: 'flex', flexDirection: 'column', gap: 6, minWidth: 0 } },
|
|
@@ -1287,9 +1346,16 @@ window.__ModuleLoader__.load({
|
|
|
1287
1346
|
React.createElement(SearchPopup, { value: search, onChange: setSearch, placeholder: '搜索文件名…' }),
|
|
1288
1347
|
React.createElement(DateFilter, { value: dayFilter, onChange: setDayFilter, placeholder: '选择日期' }),
|
|
1289
1348
|
React.createElement('label', { className: 'dsh-upload-del-toggle' },
|
|
1290
|
-
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()) } }),
|
|
1291
1350
|
'开启删除',
|
|
1292
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
|
+
),
|
|
1293
1359
|
),
|
|
1294
1360
|
error !== null && React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, error),
|
|
1295
1361
|
groups === null && error === null && React.createElement('div', { style: metaStyle }, '加载中…'),
|
|
@@ -1312,6 +1378,7 @@ window.__ModuleLoader__.load({
|
|
|
1312
1378
|
!collapsed[group.folder] && group.files.map((f) => React.createElement(
|
|
1313
1379
|
'div',
|
|
1314
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) } }),
|
|
1315
1382
|
React.createElement(
|
|
1316
1383
|
'span',
|
|
1317
1384
|
{ className: 'dsh-ws-name', style: { ...nameStyle, display: 'flex', alignItems: 'center', gap: 6 }, title: f.path },
|
|
@@ -1345,7 +1412,7 @@ window.__ModuleLoader__.load({
|
|
|
1345
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)' } }, '下载'),
|
|
1346
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)' } }, '打开'),
|
|
1347
1414
|
React.createElement('button', { type: 'button', style: delStyle, disabled: busy, onClick: () => doDelete(preview.path) }, '删除'),
|
|
1348
|
-
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 ? '还原' : '放大'),
|
|
1349
1416
|
React.createElement('button', { type: 'button', style: btnStyle, onClick: () => setPreview(null) }, '关闭'),
|
|
1350
1417
|
),
|
|
1351
1418
|
React.createElement(
|
|
@@ -1455,8 +1522,8 @@ window.__ModuleLoader__.load({
|
|
|
1455
1522
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
1456
1523
|
/** `skillDocs` namespace dictionaries. */
|
|
1457
1524
|
const zh = {
|
|
1458
|
-
"nav": "
|
|
1459
|
-
"files.hint": "
|
|
1525
|
+
"nav": "技能管理",
|
|
1526
|
+
"files.hint": "技能管理目录(可折叠,点击预览可编辑)",
|
|
1460
1527
|
"files.loading": "加载中…",
|
|
1461
1528
|
"files.empty": "目录为空",
|
|
1462
1529
|
"files.preview": "预览",
|
|
@@ -1502,19 +1569,12 @@ window.__ModuleLoader__.load({
|
|
|
1502
1569
|
/** 🔍 放大镜搜索弹窗(技能文档用):点击弹出搜索框,✕ 清除并关闭。不自动隐藏。 */
|
|
1503
1570
|
function SkillSearchPop({ value, onChange, placeholder }) {
|
|
1504
1571
|
const [open, setOpen] = react.useState(false);
|
|
1505
|
-
const [pos, setPos] = react.useState({ top: 72, left: 8 });
|
|
1506
1572
|
const ref = react.useRef(null);
|
|
1507
1573
|
react.useEffect(() => { if (open && ref.current) { try { ref.current.focus(); } catch (e) {} } }, [open]);
|
|
1508
|
-
const toggle = (
|
|
1509
|
-
if (!open && e && e.currentTarget) {
|
|
1510
|
-
const r = e.currentTarget.getBoundingClientRect();
|
|
1511
|
-
setPos({ top: r.bottom + 8, left: Math.max(8, Math.min(r.left, (window.innerWidth || 0) - 280)) });
|
|
1512
|
-
}
|
|
1513
|
-
setOpen((o) => !o);
|
|
1514
|
-
};
|
|
1574
|
+
const toggle = () => setOpen((o) => !o);
|
|
1515
1575
|
return react_jsx_runtime.jsxs("div", { className: "dsh-searchpop", children: [
|
|
1516
1576
|
react_jsx_runtime.jsx("button", { type: "button", className: "dsh-searchpop-btn", onClick: toggle, title: "搜索技能/文件", "aria-label": "搜索技能/文件", children: "🔍" }),
|
|
1517
|
-
open && react_jsx_runtime.jsxs("div", { className: "dsh-searchpop-box",
|
|
1577
|
+
open && react_jsx_runtime.jsxs("div", { className: "dsh-searchpop-box", children: [
|
|
1518
1578
|
react_jsx_runtime.jsx("input", { ref, type: "text", className: "dsh-searchpop-input", value, onChange: (e) => onChange(e.target.value), placeholder: placeholder || "搜索技能/文件…", autoFocus: true }),
|
|
1519
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: "✕" })
|
|
1520
1580
|
] })
|
|
@@ -1532,34 +1592,37 @@ window.__ModuleLoader__.load({
|
|
|
1532
1592
|
const [busy, setBusy] = react.useState(false);
|
|
1533
1593
|
const [savedFlash, setSavedFlash] = react.useState(false);
|
|
1534
1594
|
const [copiedFlash, setCopiedFlash] = react.useState(false);
|
|
1595
|
+
const [root, setRoot] = react.useState("global");
|
|
1535
1596
|
const toggleFolder = (folder) => setCollapsed((prev) => ({ ...prev, [folder]: !prev[folder] }));
|
|
1536
1597
|
|
|
1537
1598
|
const load = react.useCallback(async () => {
|
|
1538
1599
|
try {
|
|
1539
|
-
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" } });
|
|
1540
1603
|
if (!res.ok) { setError(`HTTP ${res.status}`); return; }
|
|
1541
1604
|
const data = await res.json();
|
|
1542
1605
|
if (data.ok === true) { setGroups(data.groups); setError(null); }
|
|
1543
1606
|
else setError(data.error);
|
|
1544
1607
|
}
|
|
1545
1608
|
catch (e) { setError(String((e && e.message) || e)); }
|
|
1546
|
-
}, []);
|
|
1609
|
+
}, [root]);
|
|
1547
1610
|
react.useEffect(() => { load(); }, [load]);
|
|
1548
1611
|
|
|
1549
|
-
const openDoc = async (path) => {
|
|
1550
|
-
setPreview({ path, loading: true });
|
|
1612
|
+
const openDoc = async (path, ws = "") => {
|
|
1613
|
+
setPreview({ path, loading: true, ws });
|
|
1551
1614
|
setEditing(false);
|
|
1552
1615
|
setMaximized(false);
|
|
1553
1616
|
try {
|
|
1554
|
-
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" } });
|
|
1555
1618
|
const data = await res.json();
|
|
1556
1619
|
if (data.ok === true) {
|
|
1557
|
-
setPreview(data);
|
|
1620
|
+
setPreview({ ...data, ws });
|
|
1558
1621
|
setEdited(data.content !== void 0 ? data.content : "");
|
|
1559
1622
|
}
|
|
1560
|
-
else setPreview({ path, error: data.error });
|
|
1623
|
+
else setPreview({ path, error: data.error, ws });
|
|
1561
1624
|
}
|
|
1562
|
-
catch (e) { setPreview({ path, error: String((e && e.message) || e) }); }
|
|
1625
|
+
catch (e) { setPreview({ path, error: String((e && e.message) || e), ws }); }
|
|
1563
1626
|
};
|
|
1564
1627
|
|
|
1565
1628
|
const doSave = async () => {
|
|
@@ -1569,7 +1632,7 @@ window.__ModuleLoader__.load({
|
|
|
1569
1632
|
const res = await fetch("/dsh-skill-docs/skill-doc/save", {
|
|
1570
1633
|
method: "POST",
|
|
1571
1634
|
headers: { "content-type": "application/json" },
|
|
1572
|
-
body: JSON.stringify({ path: preview.path, content: edited })
|
|
1635
|
+
body: JSON.stringify({ path: preview.path, content: edited, root, ws: preview.ws || "" })
|
|
1573
1636
|
});
|
|
1574
1637
|
const data = await res.json().catch(() => ({}));
|
|
1575
1638
|
if (data.ok === true) {
|
|
@@ -1660,6 +1723,10 @@ window.__ModuleLoader__.load({
|
|
|
1660
1723
|
return react_jsx_runtime.jsxs("div", {
|
|
1661
1724
|
style: { display: "flex", flexDirection: "column", gap: 6, minWidth: 0 },
|
|
1662
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
|
+
] }),
|
|
1663
1730
|
react_jsx_runtime.jsx("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", minWidth: 0 }, children: [
|
|
1664
1731
|
react_jsx_runtime.jsx("span", { style: { fontSize: 13, color: "var(--dsw-alias-label-tertiary)" }, children: t("files.hint") }),
|
|
1665
1732
|
react_jsx_runtime.jsx(SkillSearchPop, { value: search, onChange: setSearch, placeholder: "搜索技能/文件…" })
|
|
@@ -1684,19 +1751,20 @@ window.__ModuleLoader__.load({
|
|
|
1684
1751
|
children: [
|
|
1685
1752
|
react_jsx_runtime.jsx("span", { style: nameStyle, title: f.path, children: f.path }),
|
|
1686
1753
|
react_jsx_runtime.jsx("span", { style: metaStyle, children: `${f.size < 1024 ? `${f.size} B` : `${(f.size / 1024).toFixed(1)} KiB`}` }),
|
|
1687
|
-
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") }),
|
|
1688
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") })
|
|
1689
1756
|
]
|
|
1690
1757
|
}, f.path))
|
|
1691
1758
|
]
|
|
1692
1759
|
}, group.folder);
|
|
1693
1760
|
}),
|
|
1694
|
-
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: "没有匹配的技能管理" }),
|
|
1695
1762
|
preview !== null && react_jsx_runtime.jsxs("div", {
|
|
1696
1763
|
style: overlayStyle,
|
|
1697
1764
|
onClick: () => setPreview(null),
|
|
1698
1765
|
children: [
|
|
1699
1766
|
react_jsx_runtime.jsxs("div", {
|
|
1767
|
+
className: "dsh-skill-preview-card",
|
|
1700
1768
|
style: cardStyle,
|
|
1701
1769
|
onClick: (e) => e.stopPropagation(),
|
|
1702
1770
|
children: [
|
|
@@ -1707,7 +1775,7 @@ window.__ModuleLoader__.load({
|
|
|
1707
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") }),
|
|
1708
1776
|
editing && react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, disabled: busy, onClick: doSave, children: savedFlash ? t("files.saved") : t("files.save") }),
|
|
1709
1777
|
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: copyAll, children: copiedFlash ? t("files.copied") : t("files.copy") }),
|
|
1710
|
-
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") }),
|
|
1711
1779
|
react_jsx_runtime.jsx("button", { type: "button", style: btnStyle, onClick: () => setPreview(null), children: t("files.close") })
|
|
1712
1780
|
]
|
|
1713
1781
|
}),
|
|
@@ -1798,7 +1866,7 @@ window.__ModuleLoader__.load({
|
|
|
1798
1866
|
"files.binary": "二进制文件,无法预览,请下载后查看",
|
|
1799
1867
|
"files.truncated": "(内容过大,仅显示前 256KB)",
|
|
1800
1868
|
"files.close": "关闭预览",
|
|
1801
|
-
"skills.hint": "
|
|
1869
|
+
"skills.hint": "技能管理(各技能的 SKILL.md),可预览",
|
|
1802
1870
|
"balance": "余额",
|
|
1803
1871
|
"spend": "本会话约"
|
|
1804
1872
|
};
|
|
@@ -2380,7 +2448,7 @@ window.__ModuleLoader__.load({
|
|
|
2380
2448
|
}
|
|
2381
2449
|
const WorkspaceFilesButton = ({ sessionId, useSessions }) => {
|
|
2382
2450
|
const cur = useSessions((s) => (sessionId === void 0 ? void 0 : s.byId[sessionId]?.cwd))
|
|
2383
|
-
React.useEffect(() => { if (cur) lastCwd = normPath(cur) }, [cur])
|
|
2451
|
+
React.useEffect(() => { if (cur) { lastCwd = normPath(cur); try { window.__dshCurrentCwd = normPath(cur) } catch (e) {} } }, [cur])
|
|
2384
2452
|
// Windows 下 cwd 是反斜杠路径(C:\...\jacky),按 / 与 \ 都能切,取末段文件夹名。
|
|
2385
2453
|
const ws = cur ? normPath(cur).split('/').filter(Boolean).pop() : ''
|
|
2386
2454
|
return React.createElement(
|
|
@@ -3382,7 +3450,7 @@ window.__ModuleLoader__.load({
|
|
|
3382
3450
|
['uploadDragDrop', '附件拖放上传'],
|
|
3383
3451
|
['uploadPaste', '附件粘贴上传'],
|
|
3384
3452
|
['uploadPreview', '上传文件预览/管理'],
|
|
3385
|
-
['skillDocs', '
|
|
3453
|
+
['skillDocs', '技能管理'],
|
|
3386
3454
|
['balance', '账户余额'],
|
|
3387
3455
|
['workspace', '输出文件预览/管理'],
|
|
3388
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,8 +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
|
-
//
|
|
389
|
-
if (entry.name.startsWith(".")) continue;
|
|
388
|
+
// 排除点开头(.dsh/.outputdir)与下划线开头(_sd_extract 等中间产物)的文件夹及其下层所有内容
|
|
389
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
390
390
|
const files = [];
|
|
391
391
|
const walk = async (dir) => {
|
|
392
392
|
let sub;
|
|
@@ -399,8 +399,8 @@ async function collectGroups(root, excluded, hiddenNames = []) {
|
|
|
399
399
|
sub.sort((a, b) => a.name.localeCompare(b.name));
|
|
400
400
|
for (const item of sub) {
|
|
401
401
|
const abs = join(dir, item.name);
|
|
402
|
-
//
|
|
403
|
-
if (item.name.startsWith(".")) continue;
|
|
402
|
+
// 嵌套里的点/下划线开头条目(夹/文件)也一并排除
|
|
403
|
+
if (item.name.startsWith(".") || item.name.startsWith("_")) continue;
|
|
404
404
|
if (item.isDirectory()) {
|
|
405
405
|
await walk(abs);
|
|
406
406
|
} else if (item.isFile()) {
|
|
@@ -431,6 +431,47 @@ async function collectGroups(root, excluded, hiddenNames = []) {
|
|
|
431
431
|
return groups;
|
|
432
432
|
}
|
|
433
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
|
+
|
|
434
475
|
export async function sweepUploadTemps(root) {
|
|
435
476
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
436
477
|
const entries = await readdir(root, { withFileTypes: true });
|
|
@@ -1073,6 +1114,8 @@ export function createHandlers(options = {}) {
|
|
|
1073
1114
|
const size = info.size;
|
|
1074
1115
|
const downloadHref = `workspace-file?path=${encodeURIComponent(rel)}&download=1`;
|
|
1075
1116
|
let body;
|
|
1117
|
+
let editable = false;
|
|
1118
|
+
let rawText = "";
|
|
1076
1119
|
if (OFFICE_EXTS.has(ext)) {
|
|
1077
1120
|
const html = await officePreviewHtml(name, buffer);
|
|
1078
1121
|
body = html ? `<div class="office">${html}</div>` : `<p class="unsupported">该 Office 文档无法渲染,请下载查看。</p>`;
|
|
@@ -1085,17 +1128,20 @@ export function createHandlers(options = {}) {
|
|
|
1085
1128
|
// 避免 base64 data URI 在 iframe 内被 Chrome 拒绝。
|
|
1086
1129
|
body = `<iframe class="pdf" src="workspace-file?path=${encodeURIComponent(rel)}&inline=1"></iframe>`;
|
|
1087
1130
|
} else if (/\.(txt|log)$/i.test(name)) {
|
|
1088
|
-
|
|
1131
|
+
editable = true; rawText = buffer.toString("utf8");
|
|
1132
|
+
body = `<pre class="text">${escapeHtml(rawText)}</pre>`;
|
|
1089
1133
|
} else if (/\.(md|markdown)$/i.test(name)) {
|
|
1090
1134
|
// Markdown → 直接渲染成 HTML(真实效果),失败回退源码文本。
|
|
1091
|
-
|
|
1135
|
+
editable = true; rawText = buffer.toString("utf8");
|
|
1136
|
+
body = `<article class="md">${markdownToHtml(rawText)}</article>`;
|
|
1092
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)) {
|
|
1093
|
-
|
|
1138
|
+
editable = true; rawText = buffer.toString("utf8");
|
|
1139
|
+
body = `<pre class="text">${escapeHtml(rawText)}</pre>`;
|
|
1094
1140
|
} else {
|
|
1095
1141
|
body = `<p class="unsupported">该文件类型暂不支持预览,请点击右上角「下载」。</p>`;
|
|
1096
1142
|
}
|
|
1097
1143
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
1098
|
-
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));
|
|
1099
1145
|
} catch (error) {
|
|
1100
1146
|
sendError(res, error, onError);
|
|
1101
1147
|
}
|
|
@@ -2447,7 +2493,7 @@ function markdownToHtml(md) {
|
|
|
2447
2493
|
|
|
2448
2494
|
|
|
2449
2495
|
/** 工作区文件预览页面骨架(自包含,无外部依赖)。 */
|
|
2450
|
-
function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "") {
|
|
2496
|
+
function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "", editable = false, rawText = "") {
|
|
2451
2497
|
return `<!DOCTYPE html>
|
|
2452
2498
|
<html lang="zh-CN">
|
|
2453
2499
|
<head>
|
|
@@ -2481,6 +2527,8 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "") {
|
|
|
2481
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; }
|
|
2482
2528
|
.content { padding:20px; max-width:960px; margin:0 auto; }
|
|
2483
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); }
|
|
2484
2532
|
.md { line-height:1.8; font-size:14px; word-break:break-word; }
|
|
2485
2533
|
.md h1,.md h2,.md h3,.md h4,.md h5,.md h6 { line-height:1.45; margin:1.7em 0 .8em; }
|
|
2486
2534
|
.md h1 { font-size:1.5em; }
|
|
@@ -2527,14 +2575,36 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "") {
|
|
|
2527
2575
|
<span class="name">${escapeHtml(name)}</span>
|
|
2528
2576
|
<span class="meta">${escapeHtml(rel)} · ${(size / 1024).toFixed(1)} KB</span>
|
|
2529
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>` : ""}
|
|
2530
2580
|
${inlineHref ? `<a class="btn" href="${escapeHtml(inlineHref)}" target="_blank" rel="noopener noreferrer">${ICON_EYE} 打开</a>` : ""}
|
|
2531
2581
|
<a class="btn" href="${escapeHtml(downloadHref)}" download>${ICON_DL} 下载</a>
|
|
2532
2582
|
<button class="btn2" type="button" id="maxBtn" onclick="toggleMax()">${ICON_FOLDER} 放大</button>
|
|
2533
2583
|
<button class="btn2" type="button" onclick="closePreview()">${ICON_X} 关闭</button>
|
|
2534
2584
|
</div>
|
|
2535
2585
|
<div class="content">${body}</div>
|
|
2586
|
+
${editable ? `<textarea id="editor" class="editor">${escapeHtml(rawText)}</textarea>` : ""}
|
|
2536
2587
|
<div class="hint" id="closeHint">浏览器不允许脚本直接关闭此标签页,请手动关闭本标签页(或按 Ctrl+W / ⌘+W)。</div>
|
|
2537
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
|
+
}
|
|
2538
2608
|
function toggleMax() {
|
|
2539
2609
|
var max = document.body.classList.toggle('maximized');
|
|
2540
2610
|
document.getElementById('maxBtn').textContent = max ? '还原' : '放大';
|
|
@@ -2569,6 +2639,7 @@ function closePreview() {
|
|
|
2569
2639
|
export async function apply(ctx, config = {}) {
|
|
2570
2640
|
const trustedHosts = Array.isArray(config.trustedHosts) ? [...config.trustedHosts] : [];
|
|
2571
2641
|
const skillsRoot = resolve(config.skillsRoot ?? DEFAULT_SKILLS_ROOT);
|
|
2642
|
+
const workspaceRoot = resolveWorkspaceRoot(); // 供 skill-docs 路由"工作区技能"使用
|
|
2572
2643
|
const onError = (error) => ctx.logger.error(error instanceof Error ? error : new Error(String(error)));
|
|
2573
2644
|
|
|
2574
2645
|
const handlers = createHandlers({ trustedHosts, onError, excludedWorkspaceNames: config.excludedWorkspaceNames });
|
|
@@ -2783,6 +2854,19 @@ export async function apply(ctx, config = {}) {
|
|
|
2783
2854
|
}), "dsh-long-plugins: workspace rename route");
|
|
2784
2855
|
|
|
2785
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
|
+
|
|
2786
2870
|
ctx.effect(() => ctx.webServer.register({
|
|
2787
2871
|
kind: "exact",
|
|
2788
2872
|
path: "/dsh-skill-docs/skill-docs",
|
|
@@ -2793,8 +2877,7 @@ export async function apply(ctx, config = {}) {
|
|
|
2793
2877
|
}
|
|
2794
2878
|
try {
|
|
2795
2879
|
requireTrusted(req);
|
|
2796
|
-
|
|
2797
|
-
sendJson(res, 200, { ok: true, root: skillsRoot, groups });
|
|
2880
|
+
await skillDocsList(req, res);
|
|
2798
2881
|
} catch (error) {
|
|
2799
2882
|
sendError(res, error, onError);
|
|
2800
2883
|
}
|
|
@@ -2817,7 +2900,10 @@ export async function apply(ctx, config = {}) {
|
|
|
2817
2900
|
}
|
|
2818
2901
|
try {
|
|
2819
2902
|
requireTrusted(req);
|
|
2820
|
-
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);
|
|
2821
2907
|
if (full === undefined) throw new HttpError(400, "bad path");
|
|
2822
2908
|
const info = await stat(full);
|
|
2823
2909
|
if (!info.isFile()) throw new HttpError(400, "not a file");
|
|
@@ -2869,8 +2955,11 @@ export async function apply(ctx, config = {}) {
|
|
|
2869
2955
|
const body = await readJsonBody(req);
|
|
2870
2956
|
const rel = typeof body === "object" && body !== null ? body.path : undefined;
|
|
2871
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;
|
|
2872
2961
|
if (typeof content !== "string") throw new HttpError(400, "content required");
|
|
2873
|
-
const full = safeResolve(
|
|
2962
|
+
const full = safeResolve(root, rel);
|
|
2874
2963
|
if (full === undefined) throw new HttpError(400, "bad path");
|
|
2875
2964
|
const info = await stat(full);
|
|
2876
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 等确认。
|