dsh-long-plugins 2.4.2 → 2.4.6

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 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((m) => !m) }, previewMaximized ? '还原' : '放大'),
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:nowrap;justify-content:flex-start}.dsh-upload-head-actions .dsh-searchpop,.dsh-upload-head-actions .dsh-datefilter{flex:1 1 0;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{flex:none;white-space:nowrap;padding:0 4px}}
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((m) => !m) }, maximized ? '还原' : '放大'),
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 = (e) => {
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", style: { top: pos.top, left: pos.left }, children: [
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 res = await fetch("/dsh-skill-docs/skill-docs", { headers: { Accept: "application/json" } });
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((m) => !m), children: maximized ? t("files.restore") : t("files.maximize") }),
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": "技能文档(各技能的 SKILL.md),可预览",
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(
@@ -3377,12 +3445,11 @@ window.__ModuleLoader__.load({
3377
3445
  // 「dsh-long」设置区:各模块开关(独立、移动端干净),避免把开关塞进 RA-Span
3378
3446
  const DSH_LONG_MODULES = [
3379
3447
  ['glass', 'RA-Span'],
3380
- ['turnRuler', '会话导航(轮次刻尺)'],
3381
3448
  ['uploadAttach', '附件上传(回形针)'],
3382
3449
  ['uploadDragDrop', '附件拖放上传'],
3383
3450
  ['uploadPaste', '附件粘贴上传'],
3384
3451
  ['uploadPreview', '上传文件预览/管理'],
3385
- ['skillDocs', '技能文档'],
3452
+ ['skillDocs', '技能管理'],
3386
3453
  ['balance', '账户余额'],
3387
3454
  ['workspace', '输出文件预览/管理'],
3388
3455
  ['mobile', '移动端布局'],
@@ -3867,7 +3934,6 @@ window.__ModuleLoader__.load({
3867
3934
  try {
3868
3935
  const tk = dshDark() ? 'dark' : 'light'
3869
3936
  root.setAttribute('data-dsh-theme', tk)
3870
- root.setAttribute('data-dsh-turnruler', ((c.modules && c.modules.turnRuler) !== false) ? 'on' : 'off')
3871
3937
  window.__dshLongModules = c.modules || {}
3872
3938
  try { localStorage.setItem('dsh-long:modules', JSON.stringify(window.__dshLongModules)) } catch (e) {}
3873
3939
  const bt = bgTint(tk)
@@ -3968,7 +4034,6 @@ window.__ModuleLoader__.load({
3968
4034
  ...tokenUsagePlugin.inject,
3969
4035
  ...mobilePlugin.inject,
3970
4036
  ...workspaceFilesPlugin.inject,
3971
- ...turnRulerPlugin.inject,
3972
4037
  ...glassPlugin.inject,
3973
4038
  ]))
3974
4039
 
@@ -4006,7 +4071,6 @@ window.__ModuleLoader__.load({
4006
4071
  safeApply('token-usage', (c) => tokenUsagePlugin.apply(c)) // 余额 chip 开关在其内部; MOBILE_CSS 始终注入
4007
4072
  if (modEnabled('mobile')) safeApply('mobile-hamburger', (c) => mobilePlugin.apply(c))
4008
4073
  if (modEnabled('workspace')) safeApply('workspace-files', (c) => workspaceFilesPlugin.apply(c))
4009
- if (modEnabled('turnRuler')) safeApply('turn-ruler', (c) => turnRulerPlugin.apply(c))
4010
4074
  if (modEnabled('glass')) safeApply('glass', (c) => glassPlugin.apply(c))
4011
4075
  }
4012
4076
 
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.2",
4
+ "version": "2.4.6",
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
- // 排除点开头的文件夹(.dsh / .outputdir)及其下层所有内容
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
- body = `<pre class="text">${escapeHtml(buffer.toString("utf8"))}</pre>`;
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
- body = `<article class="md">${markdownToHtml(buffer.toString("utf8"))}</article>`;
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
- body = `<pre class="text">${escapeHtml(buffer.toString("utf8"))}</pre>`;
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
- const groups = await collectGroups(skillsRoot, "__dsh_none__");
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 full = safeResolve(skillsRoot, rel);
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(skillsRoot, rel);
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.2",
3
+ "version": "2.4.6",
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 等确认。