dsh-long-plugins 1.3.7 → 1.3.9

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
@@ -73,6 +73,7 @@ Add to the profile's `cordis.patch.yml`:
73
73
  - `trustedHosts` — required for the browser trust fence (same as the built-in Web API).
74
74
  - `skillsRoot` — root directory browsed by the「技能文档」section. Defaults to `<DSH_HOME>/skills`.
75
75
  - `md2docxScript` — (optional) override the Markdown→Word script path. Defaults to the bundled `lib/md2docx.py`.
76
+ - `excludedWorkspaceNames` — (optional) extra file names to hide from the workspace file browse / 输出文件 (deployment & plugin config clutter). The plugin already hides a default blocklist (`index.html`, `serve.log`, `docker-compose.*`, `package*.json`, `*.log`, `*.lock`, `Dockerfile`, …); add your own here, e.g. `excludedWorkspaceNames: ['jackylong1987__dsh-long-plugins.yml']`. This only hides the listed names — AI-generated `.yml`/`.html`/`.json` reports still show.
76
77
 
77
78
  ## md2docx 工具(Markdown → Word)
78
79
 
package/client/client.js CHANGED
@@ -81,6 +81,35 @@ window.__ModuleLoader__.load({
81
81
  return base.replace(/\.[^.]+$/, '')
82
82
  }
83
83
 
84
+ /** 轻量 toast:短暂显示一条消息(用于「粘贴的文件格式不支持」等提示)。 */
85
+ function showToast(text, kind) {
86
+ let t = document.querySelector('.dsh-long-toast')
87
+ if (!t) {
88
+ t = document.createElement('div')
89
+ t.className = 'dsh-long-toast'
90
+ document.body.appendChild(t)
91
+ }
92
+ t.textContent = text
93
+ t.className = 'dsh-long-toast ' + (kind || '') + ' show'
94
+ clearTimeout(t._timer)
95
+ t._timer = setTimeout(() => { t.className = 'dsh-long-toast' }, 2800)
96
+ }
97
+
98
+ /** 复制文本到剪贴板 + 轻提示。 */
99
+ function copyText(text) {
100
+ try {
101
+ const done = () => showToast(`已复制:${text}`)
102
+ if (navigator.clipboard && navigator.clipboard.writeText) {
103
+ navigator.clipboard.writeText(text).then(done, () => showToast('复制失败', 'error'))
104
+ } else {
105
+ const ta = document.createElement('textarea')
106
+ ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'
107
+ document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta)
108
+ done()
109
+ }
110
+ } catch (e) { showToast('复制失败', 'error') }
111
+ }
112
+
84
113
  /** Trigger a browser download for a URL (no navigation, keeps the page). */
85
114
  function triggerDownload(url, name) {
86
115
  const a = document.createElement('a')
@@ -469,15 +498,48 @@ window.__ModuleLoader__.load({
469
498
  }
470
499
  }
471
500
  }
501
+ // 粘贴非图片文件 → 走与回形针/拖放相同的上传管线,并阻止 DSH 核心的「仅支持图片」门;
502
+ // 纯文本/图片粘贴放行给 DSH 原生;上传失败(不支持的格式等)给可见提示。
503
+ const onPaste = async (e) => {
504
+ const cd = e.clipboardData
505
+ if (!cd) return
506
+ const files = []
507
+ const items = cd.items
508
+ if (items) {
509
+ for (const item of items) {
510
+ if (item.kind === 'file' && typeof item.getAsFile === 'function') {
511
+ const f = item.getAsFile()
512
+ // 只接管非图片文件;图片交给 DSH 原生(内联显示),避免改变已有行为
513
+ if (f && f.size > 0 && !(f.type || '').startsWith('image/')) files.push(f)
514
+ }
515
+ }
516
+ }
517
+ if (files.length === 0) return // 无「非图片文件」→ 放行给 DSH
518
+ // 有文件粘贴:接管整段粘贴,阻止 DSH 后续处理器报「仅支持图片」
519
+ e.preventDefault()
520
+ e.stopPropagation()
521
+ e.stopImmediatePropagation()
522
+ for (const file of files) {
523
+ try {
524
+ const stored = await uploadFile(file)
525
+ controller.attach(sessionId, stored)
526
+ } catch (error) {
527
+ console.warn(`[dsh-long-plugins] 粘贴上传失败:${file.name} → ${errorMessage(error)}`)
528
+ showToast(`不支持该文件格式:${file.name}`, 'error')
529
+ }
530
+ }
531
+ }
472
532
  window.addEventListener('dragenter', onDragEnter, true)
473
533
  window.addEventListener('dragover', onDragOver, true)
474
534
  window.addEventListener('dragleave', onDragLeave, true)
475
535
  window.addEventListener('drop', onDrop, true)
536
+ window.addEventListener('paste', onPaste, true)
476
537
  return () => {
477
538
  window.removeEventListener('dragenter', onDragEnter, true)
478
539
  window.removeEventListener('dragover', onDragOver, true)
479
540
  window.removeEventListener('dragleave', onDragLeave, true)
480
541
  window.removeEventListener('drop', onDrop, true)
542
+ window.removeEventListener('paste', onPaste, true)
481
543
  }
482
544
  }, [controller, sessionId])
483
545
 
@@ -881,6 +943,15 @@ window.__ModuleLoader__.load({
881
943
  { className: 'dsh-upload-actions' },
882
944
  React.createElement('button', { type: 'button', className: 'dsh-upload-preview', onClick: () => previewFile(file.name) }, '预览'),
883
945
  React.createElement('a', { href: downloadUrl(file.name), download: file.name }, '下载'),
946
+ React.createElement(
947
+ 'button',
948
+ { type: 'button', onClick: () => {
949
+ const s = String(file.path || file.name)
950
+ const base = state.root ? String(state.root).replace(/\/[^/]*$/, '') : ''
951
+ copyText(base && s.indexOf(base) === 0 ? s.slice(base.length).replace(/^\/+/, '') : s)
952
+ } },
953
+ '复制路径',
954
+ ),
884
955
  React.createElement(
885
956
  'button',
886
957
  { type: 'button', disabled: deleting === file.name, onClick: () => remove(file.name) },
@@ -948,12 +1019,13 @@ window.__ModuleLoader__.load({
948
1019
  .dsh-upload-group-day span{font-weight:400;color:var(--dsw-alias-label-tertiary)}
949
1020
  .dsh-upload-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l2);border-radius:8px;font-size:13px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform,transparent);margin-bottom:4px}
950
1021
  .dsh-upload-file-name{flex:1;min-width:0;display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-primary)}
951
- .dsh-upload-file-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1022
+ .dsh-upload-file-label{min-width:0;white-space:normal;word-break:break-all;overflow-wrap:anywhere}
952
1023
  .dsh-upload-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}
953
1024
  .dsh-upload-file-meta{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px}
954
1025
  .dsh-upload-actions{display:flex;flex:none;gap:7px}
955
1026
  .dsh-upload-actions button{color:var(--dsw-alias-state-error-primary)}
956
- @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%}.dsh-upload-actions a,.dsh-upload-actions button{flex:1;text-align:center}.dsh-upload-chip{min-width:160px}.dsh-upload-settings-head{flex-direction:column;align-items:stretch;gap:10px}.dsh-upload-head-actions{width:100%;justify-content:flex-end}.dsh-upload-head-actions .dsh-upload-refresh{flex:1;text-align:center;padding:7px 8px;max-width:none}}
1027
+ .dsh-upload-actions a,.dsh-upload-actions button{white-space:nowrap}
1028
+ @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%;justify-content:flex-end}.dsh-upload-head-actions .dsh-upload-refresh{flex:1;text-align:center;padding:7px 8px;max-width:none}}
957
1029
  .dsh-ws-folder:hover{background:var(--dsw-alias-interactive-bg-hover)}
958
1030
  .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}
959
1031
  .dsh-upload-preview-del{color:var(--dsw-alias-state-error-primary)!important}
@@ -981,6 +1053,9 @@ window.__ModuleLoader__.load({
981
1053
  .dsh-upload-preview-open:hover{background:var(--dsw-alias-interactive-bg-hover)}
982
1054
  .dsh-upload-preview-img{max-width:100%;max-height:70vh;object-fit:contain;display:block}
983
1055
  .dsh-upload-preview-loading{display:flex;align-items:center;justify-content:center;flex:1;min-height:120px;color:var(--dsw-alias-label-secondary);font-size:13px}
1056
+ .dsh-long-toast{position:fixed;left:50%;bottom:36px;transform:translateX(-50%) translateY(12px);z-index:2000;max-width:min(90vw,520px);padding:10px 16px;border-radius:10px;background:var(--dsw-specific-input-major,#0f1720);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;box-shadow:var(--dsw-shadow-lv3);opacity:0;pointer-events:none;transition:opacity .2s ease,transform .2s ease}
1057
+ .dsh-long-toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
1058
+ .dsh-long-toast.error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
984
1059
  `
985
1060
 
986
1061
  const inject = ['slots', 'sessions', 'inputTriggers', 'conversation', 'timer']
@@ -1139,7 +1214,7 @@ window.__ModuleLoader__.load({
1139
1214
  borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)',
1140
1215
  background: 'var(--dsw-alias-bg-module-platform, transparent)', fontSize: 13,
1141
1216
  }
1142
- const nameStyle = { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'var(--dsw-alias-label-primary)' }
1217
+ const nameStyle = { flex: 1, minWidth: 0, whiteSpace: 'normal', wordBreak: 'break-all', overflowWrap: 'anywhere', color: 'var(--dsw-alias-label-primary)' }
1143
1218
  const metaStyle = { flex: 'none', color: 'var(--dsw-alias-label-tertiary)', fontSize: 12 }
1144
1219
  const btnStyle = { flex: 'none', cursor: 'pointer', border: 'none', borderRadius: 6, padding: '3px 8px', fontSize: 12, background: 'var(--dsw-alias-interactive-bg-hover)', color: 'var(--dsw-alias-label-secondary)' }
1145
1220
  const delStyle = { ...btnStyle, color: 'var(--dsw-alias-state-error-primary)' }
@@ -1217,7 +1292,7 @@ window.__ModuleLoader__.load({
1217
1292
  React.createElement(
1218
1293
  'span',
1219
1294
  { className: 'dsh-ws-name', style: { ...nameStyle, display: 'flex', alignItems: 'center', gap: 6 }, title: f.path },
1220
- React.createElement('span', { className: 'dsh-ws-file-label', style: { flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, basenameWithoutExt(f.path)),
1295
+ React.createElement('span', { className: 'dsh-ws-file-label', style: { flex: 1, minWidth: 0, whiteSpace: 'normal', wordBreak: 'break-all', overflowWrap: 'anywhere' } }, basenameWithoutExt(f.path)),
1221
1296
  React.createElement('span', { className: 'dsh-ws-file-type', style: { flex: 'none' } }, fileTypeLabel(f.path)),
1222
1297
  ),
1223
1298
  React.createElement('span', { className: 'dsh-ws-meta', style: metaStyle }, `${sizeText(f.size)} · ${dateText(f.mtime)}`),
@@ -1226,6 +1301,7 @@ window.__ModuleLoader__.load({
1226
1301
  { className: 'dsh-ws-actions', style: { display: 'flex', gap: 6, flex: 'none' } },
1227
1302
  React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: () => openPreview(f.path) }, '预览'),
1228
1303
  React.createElement('a', { href: '/api/dsh-uploads/workspace-file?path=' + encodeURIComponent(f.path) + '&download=1', download: f.name, style: btnStyle }, '下载'),
1304
+ React.createElement('button', { type: 'button', style: btnStyle, disabled: busy, onClick: () => copyText(f.path) }, '复制路径'),
1229
1305
  React.createElement('button', { type: 'button', style: delStyle, disabled: busy, onClick: () => doDelete(f.path) }, '删除'),
1230
1306
  ),
1231
1307
  )),
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": "1.3.7",
4
+ "version": "1.3.9",
5
5
  "entry": {
6
6
  "name": "dsh-long-plugins",
7
7
  "inject": [
package/lib/index.js CHANGED
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { createReadStream } from "node:fs";
16
16
  import { readFile, writeFile } from "node:fs/promises";
17
- import { link, lstat, mkdir, open, readdir, stat, unlink } from "node:fs/promises";
17
+ import { link, lstat, mkdir, open, readdir, rename, stat, unlink } from "node:fs/promises";
18
18
  import { homedir } from "node:os";
19
19
  import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
@@ -348,8 +348,27 @@ const DOCUMENT_EXTS = new Set([
348
348
  "png", "jpg", "jpeg", "gif", "webp", "bmp", "svg", "ico", "tiff", "heic",
349
349
  ]);
350
350
 
351
+ /** 默认隐藏的「非会话产出」文件(部署/插件/日志等混入工作区的杂项),
352
+ * 浏览页与输出文件面板都隐藏;可用 config.excludedWorkspaceNames 追加。 */
353
+ const WORKSPACE_EXCLUDED_NAMES = new Set([
354
+ "index.html", "index.htm", "serve.log", "docker-compose.yml", "docker-compose.yaml",
355
+ "compose.yml", "compose.yaml", "package.json", "package-lock.json", "pnpm-lock.yaml",
356
+ "yarn.lock", "bun.lockb", "Dockerfile", ".dockerignore", ".gitignore", ".npmrc",
357
+ "server.mjs", "server.js", "start.sh", "start-at-boot.sh",
358
+ ]);
359
+ const WORKSPACE_EXCLUDED_SUFFIXES = [".log", ".lock"];
360
+
361
+ /** Whether a file name should be hidden from workspace listings (case-insensitive). */
362
+ function isExcludedWorkspaceName(name, extra = []) {
363
+ const lower = String(name).toLowerCase();
364
+ if (WORKSPACE_EXCLUDED_NAMES.has(lower)) return true;
365
+ for (const s of WORKSPACE_EXCLUDED_SUFFIXES) if (lower.endsWith(s)) return true;
366
+ for (const n of extra) if (lower === String(n).toLowerCase()) return true;
367
+ return false;
368
+ }
369
+
351
370
  /** Collect subdirectories with their files, grouped by folder. */
352
- async function collectGroups(root, excluded) {
371
+ async function collectGroups(root, excluded, hiddenNames = []) {
353
372
  let entries;
354
373
  try {
355
374
  entries = await readdir(root, { withFileTypes: true });
@@ -375,6 +394,8 @@ async function collectGroups(root, excluded) {
375
394
  if (item.isDirectory()) {
376
395
  await walk(abs);
377
396
  } else if (item.isFile()) {
397
+ // 隐藏明确的「非会话产出」文件(部署/插件/日志等),再走文档类白名单
398
+ if (isExcludedWorkspaceName(item.name, hiddenNames)) continue;
378
399
  // 白名单:只显示文档类 + 图片文件,脚本/压缩包/代码/音视频一律隐藏
379
400
  const ext = item.name.slice(item.name.lastIndexOf(".") + 1).toLowerCase();
380
401
  if (!DOCUMENT_EXTS.has(ext)) continue;
@@ -590,6 +611,7 @@ export function createHandlers(options = {}) {
590
611
  const maxFileBytes = positiveInteger(options.maxFileBytes, resolveMaxFileBytes());
591
612
  const totalMaxBytes = positiveInteger(options.totalMaxBytes, resolveTotalMaxBytes());
592
613
  const trustedHosts = Array.isArray(options.trustedHosts) ? [...options.trustedHosts] : [];
614
+ const hiddenNames = Array.isArray(options.excludedWorkspaceNames) ? [...options.excludedWorkspaceNames] : [];
593
615
  const onError = typeof options.onError === "function" ? options.onError : undefined;
594
616
  for (const entry of trustedHosts) assertTrustedAuthority(entry);
595
617
 
@@ -748,7 +770,7 @@ export function createHandlers(options = {}) {
748
770
  methodNotAllowed(res, ["GET", "HEAD"]);
749
771
  return;
750
772
  }
751
- const groups = await collectGroups(workspaceRoot, workspaceExcluded);
773
+ const groups = await collectGroups(workspaceRoot, workspaceExcluded, hiddenNames);
752
774
  sendJson(res, 200, { ok: true, root: workspaceRoot, groups });
753
775
  } catch (error) {
754
776
  sendError(res, error, onError);
@@ -843,6 +865,34 @@ export function createHandlers(options = {}) {
843
865
  }
844
866
  };
845
867
 
868
+ // 重命名工作区文件:只改文件名(限定同目录,不跨目录移动),低风险。
869
+ const workspaceRename = async (req, res) => {
870
+ try {
871
+ requireTrusted(req);
872
+ if (req.method !== "POST") {
873
+ methodNotAllowed(res, ["POST"]);
874
+ return;
875
+ }
876
+ const body = await readJsonBody(req);
877
+ const rel = typeof body === "object" && body !== null ? body.path : undefined;
878
+ const newName = typeof body === "object" && body !== null ? body.newName : undefined;
879
+ const full = safeResolve(workspaceRoot, rel);
880
+ if (full === undefined) throw new HttpError(400, "invalid path");
881
+ const info = await stat(full);
882
+ if (!info.isFile()) throw new HttpError(400, "not a regular file");
883
+ // 新文件名必须是安全的纯文件名(无分隔符/路径穿越/隐藏/特殊)
884
+ if (!isSafeStoredName(newName)) throw new HttpError(400, "invalid name");
885
+ const newRel = join(dirname(rel), newName);
886
+ const newFull = safeResolve(workspaceRoot, newRel);
887
+ if (newFull === undefined || newFull === full) throw new HttpError(400, "invalid target");
888
+ // 同目录纯改名:rename 原子且安全(不跨目录移动)
889
+ await rename(full, newFull);
890
+ sendJson(res, 200, { ok: true, path: newRel });
891
+ } catch (error) {
892
+ sendError(res, error, onError);
893
+ }
894
+ };
895
+
846
896
  const workspaceSave = async (req, res) => {
847
897
  try {
848
898
  requireTrusted(req);
@@ -927,7 +977,7 @@ export function createHandlers(options = {}) {
927
977
  const params = new URL(req.url || "/", "http://dsh.internal").searchParams;
928
978
  const ws = params.get("ws") || "";
929
979
  const all = params.get("all") === "1";
930
- const groups = await collectGroups(workspaceRoot, workspaceExcluded);
980
+ const groups = await collectGroups(workspaceRoot, workspaceExcluded, hiddenNames);
931
981
  const view = all ? groups : (ws ? groups.filter((g) => g.folder === ws) : groups);
932
982
  res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
933
983
  res.end(workspaceBrowseHtml(view, ws, all));
@@ -1220,7 +1270,7 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1220
1270
  }
1221
1271
  };
1222
1272
 
1223
- return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage };
1273
+ return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage };
1224
1274
  }
1225
1275
 
1226
1276
  /** 人类可读文件大小。 */
@@ -1280,13 +1330,15 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
1280
1330
  const viewHref = previewable
1281
1331
  ? (isPdf ? `workspace-file?path=${rel}&inline=1` : isDocx ? `docx-preview?path=${rel}` : isPptx ? `pptx-preview?path=${rel}` : `workspace-preview?path=${rel}&from=list`)
1282
1332
  : `workspace-file?path=${rel}&download=1`;
1283
- return `<tr data-ts="${Math.floor(f.mtime)}" data-name="${escapeHtml(f.name)}">
1333
+ return `<tr data-ts="${Math.floor(f.mtime)}" data-name="${escapeHtml(f.name)}" data-size="${f.size}">
1284
1334
  <td class="name"><a class="flink" href="${viewHref}" data-preview="${escapeHtml(viewHref)}" title="${escapeHtml(f.path)}">${escapeHtml(f.name)}</a></td>
1285
1335
  <td class="size">${humanSize(f.size)}</td>
1286
1336
  <td class="time">${humanTime(f.mtime)}</td>
1287
1337
  <td class="acts">
1288
1338
  ${previewable ? `<a class="tag" href="${viewHref}" data-preview="${escapeHtml(viewHref)}">${ICON_EYE} 预览</a>` : `<a class="tag" href="${viewHref}">${ICON_DL} 下载</a>`}
1289
1339
  <a class="tag dl" href="workspace-file?path=${rel}&download=1">${ICON_DL} 下载</a>
1340
+ <button type="button" class="tag" data-rename="${escapeHtml(f.path)}">重命名</button>
1341
+ <button type="button" class="tag" data-copypath="${escapeHtml(f.path)}">复制路径</button>
1290
1342
  </td>
1291
1343
  </tr>`;
1292
1344
  }).join("");
@@ -1331,6 +1383,19 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
1331
1383
  .btn { display:inline-flex; align-items:center; gap:6px; background:var(--lp-btn-bg); color:var(--lp-btn-fg); text-decoration:none; border-radius:8px; padding:7px 14px; font-size:13px; flex:none; }
1332
1384
  .btn:hover { background:var(--lp-btn-hover); }
1333
1385
  .wrap { max-width:980px; margin:0 auto; padding:20px; }
1386
+ .radial { position:fixed; right:22px; bottom:22px; width:0; height:0; z-index:60; }
1387
+ .radial-center { position:absolute; left:0; top:0; transform:translate(-50%,-50%); width:52px; height:52px; border-radius:50%; border:1px solid var(--lp-border); background:var(--lp-bar-bg); color:var(--lp-fg); font-size:20px; line-height:1; cursor:pointer; display:flex; align-items:center; justify-content:center; box-shadow:var(--lp-shadow-lv2, 0 6px 18px rgba(0,0,0,.35)); }
1388
+ .radial-center:hover { border-color:var(--lp-accent); }
1389
+ .radial-item { position:absolute; left:0; top:0; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:1px; width:52px; height:52px; border-radius:50%; border:1px solid var(--lp-border); background:var(--lp-bar-bg); color:var(--lp-fg); padding:0; font:inherit; cursor:pointer; opacity:0; pointer-events:none; transition:transform .18s ease, opacity .18s ease; }
1390
+ .radial-item b { font-size:15px; line-height:1; font-variant-numeric:tabular-nums; }
1391
+ .radial-item span { font-size:10px; color:var(--lp-meta); }
1392
+ .radial-item.on { border-color:var(--lp-accent); }
1393
+ .radial-item.on b { color:var(--lp-accent); }
1394
+ .radial.expanded .radial-item { opacity:1; pointer-events:auto; }
1395
+ .radial.expanded .ri-today { transform:translate(-50%,-50%) translate(0,-78px); }
1396
+ .radial.expanded .ri-week { transform:translate(-50%,-50%) translate(-56px,-56px); }
1397
+ .radial.expanded .ri-all { transform:translate(-50%,-50%) translate(-78px,0); }
1398
+ @media (max-width: 640px) { .radial { right:14px; bottom:14px; } .radial-center, .radial-item { width:46px; height:46px; } }
1334
1399
  .group { margin-bottom:26px; }
1335
1400
  .group h2 { font-size:15px; margin:0 0 8px; color:var(--lp-h2); }
1336
1401
  .cnt { color:var(--lp-meta); font-size:12px; font-weight:400; }
@@ -1339,8 +1404,7 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
1339
1404
  .group.collapsed .gtoggle:before { content:'▸'; }
1340
1405
  .group.collapsed table { display:none; }
1341
1406
  .group.filtered-empty { display:none; }
1342
- .daybox { display:inline-flex; align-items:center; gap:8px; flex:none; }
1343
- .daylabel { color:var(--lp-meta); font-size:13px; }
1407
+ .daybox { display:inline-flex; align-items:center; gap:8px; flex:none; } .daylabel { color:var(--lp-meta); font-size:13px; }
1344
1408
  .dfilter { position:relative; display:inline-flex; align-items:center; gap:6px; min-width:118px; border:1px solid var(--lp-border); border-radius:8px; background:var(--lp-btn-bg); color:var(--lp-fg); padding:5px 8px; font-size:13px; cursor:pointer; }
1345
1409
  .dfilter:hover { border-color:var(--lp-accent); }
1346
1410
  .dfilter.has-value { border-color:var(--lp-accent); }
@@ -1358,6 +1422,9 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
1358
1422
  .fsearch:focus { outline:none; border-color:var(--lp-accent); }
1359
1423
  .fsearch-clear { background:transparent; border:none; color:var(--lp-meta); font-size:13px; line-height:1; cursor:pointer; padding:0 3px; }
1360
1424
  .fsearch-clear:hover { color:var(--lp-fg); }
1425
+ .wsb-toast { position:fixed; left:50%; bottom:32px; transform:translateX(-50%) translateY(12px); z-index:2000; max-width:min(90vw,520px); padding:10px 16px; border-radius:10px; background:var(--lp-bar-bg); border:1px solid var(--lp-border); color:var(--lp-fg); font-size:13px; line-height:20px; opacity:0; pointer-events:none; transition:opacity .2s ease,transform .2s ease; box-shadow:0 10px 30px rgba(0,0,0,.35); }
1426
+ .wsb-toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
1427
+ .wsb-toast.error { border-color:#e25050; color:#ffb4b4; }
1361
1428
  .btn2 { display:inline-flex; align-items:center; background:var(--lp-btn-bg); color:var(--lp-btn-fg); border:1px solid var(--lp-border); border-radius:8px; padding:6px 11px; font-size:12px; cursor:pointer; }
1362
1429
  .btn2:hover { background:var(--lp-btn-hover); }
1363
1430
  .daybtn.active { background:#2563eb; color:#fff; }
@@ -1423,6 +1490,12 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
1423
1490
  ${groups.map(section).join("") || `<p class="empty">${ws && !all ? `工作区「${escapeHtml(ws)}」还没有文件` : "工作区还没有文件"}</p>`}
1424
1491
  <p class="empty" id="filterEmpty" style="display:none">没有匹配的文件</p>
1425
1492
  </div>
1493
+ <div class="radial" id="radial">
1494
+ <button type="button" class="radial-center" id="radialBtn" title="工作区概览" aria-label="工作区概览">📊</button>
1495
+ <button type="button" class="radial-item ri-today" id="radialToday" title="只看今天生成的文件"><b id="dashTodayN">–</b><span>今日</span></button>
1496
+ <button type="button" class="radial-item ri-week" id="radialWeek" title="只看最近7天生成的文件"><b id="dashWeekN">–</b><span>本周</span></button>
1497
+ <button type="button" class="radial-item ri-all" id="radialAll" title="显示全部文件"><b id="dashTotalN">–</b><span>全部</span></button>
1498
+ </div>
1426
1499
  <script>
1427
1500
  // 预览链接点击 → 通知父窗口(wsOverlay)打开,父窗口用 embed/iframe 渲染,
1428
1501
  // 避免在嵌套 iframe 内直接导航导致 PDF 无法滚动。
@@ -1483,13 +1556,17 @@ document.addEventListener('click', function (e) {
1483
1556
  tr.dataset.date = fmtDate(Number(tr.dataset.ts));
1484
1557
  });
1485
1558
  var todayStr = fmtDate(Date.now());
1559
+ var weekOn = false; // 近7天范围筛选
1486
1560
  function apply() {
1487
1561
  var sel = dayInput.value;
1562
+ var nowTs = Date.now();
1488
1563
  var visTotal = 0;
1489
1564
  groups.forEach(function (g) {
1490
1565
  var vis = 0;
1491
1566
  Array.prototype.forEach.call(g.querySelectorAll('tr[data-date]'), function (tr) {
1492
- var ok = (!sel || tr.dataset.date === sel) && matchName(tr);
1567
+ var dayOk = !sel || tr.dataset.date === sel;
1568
+ var weekOk = !weekOn || (nowTs - Number(tr.dataset.ts)) <= 7 * 86400000;
1569
+ var ok = dayOk && weekOk && matchName(tr);
1493
1570
  tr.style.display = ok ? '' : 'none';
1494
1571
  if (ok) vis++;
1495
1572
  });
@@ -1499,12 +1576,13 @@ document.addEventListener('click', function (e) {
1499
1576
  visTotal += vis;
1500
1577
  });
1501
1578
  var searching = fsearch && fsearch.value.trim();
1502
- if (metaEl) metaEl.textContent = (sel || searching) ? ('筛选 ' + (sel || '日期') + (searching ? ' · ' + searching : '') + ' · ' + visTotal + ' 个文件') : origMeta;
1579
+ var label = sel ? ('筛选 ' + sel) : (weekOn ? '近7日' : '');
1580
+ if (metaEl) metaEl.textContent = (label || searching) ? ((label || '全部') + (searching ? ' · ' + searching : '') + ' · ' + visTotal + ' 个文件') : origMeta;
1503
1581
  if (dfilterText) dfilterText.textContent = sel || '选择日期';
1504
1582
  if (dfilterClear) dfilterClear.style.display = sel ? 'inline-block' : 'none';
1505
1583
  if (dfilter) dfilter.classList.toggle('has-value', !!sel);
1506
1584
  if (dayToday) dayToday.classList.toggle('active', sel === todayStr);
1507
- if (filterEmpty) filterEmpty.style.display = ((sel || searching) && visTotal === 0) ? 'block' : 'none';
1585
+ if (filterEmpty) filterEmpty.style.display = ((label || searching) && visTotal === 0) ? 'block' : 'none';
1508
1586
  }
1509
1587
  // 打开日期选择器:整个控件点击 → showPicker(兜底 focus);点 ✕ 清除(阻止冒泡)。
1510
1588
  if (dfilter) dfilter.addEventListener('click', function () {
@@ -1542,6 +1620,77 @@ document.addEventListener('click', function (e) {
1542
1620
  if (dfilterClear) dfilterClear.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); dayInput.value = ''; apply(); });
1543
1621
  if (savedOpen) setFsearchOpen(true);
1544
1622
  apply();
1623
+
1624
+ // 重命名 / 复制路径
1625
+ function showMsg(text, kind) {
1626
+ var t = document.querySelector('.wsb-toast');
1627
+ if (!t) {
1628
+ t = document.createElement('div');
1629
+ t.className = 'wsb-toast';
1630
+ document.body.appendChild(t);
1631
+ }
1632
+ t.textContent = text;
1633
+ t.className = 'wsb-toast show' + (kind ? ' ' + kind : '');
1634
+ clearTimeout(t._t);
1635
+ t._t = setTimeout(function () { t.className = 'wsb-toast'; }, 2600);
1636
+ }
1637
+ document.addEventListener('click', function (e) {
1638
+ var t = e.target && e.target.closest ? e.target.closest('[data-rename]') : null;
1639
+ var c = e.target && e.target.closest ? e.target.closest('[data-copypath]') : null;
1640
+ if (t) {
1641
+ e.preventDefault(); e.stopPropagation();
1642
+ var rel = t.getAttribute('data-rename');
1643
+ var name = rel.split(/[\\/]/).pop() || rel;
1644
+ var nn = window.prompt('重命名文件(仅改名,不跨目录)', name);
1645
+ if (nn == null || String(nn).trim() === '' || String(nn).trim() === name) return;
1646
+ fetch('/api/dsh-uploads/workspace-file/rename', {
1647
+ method: 'POST', headers: { 'content-type': 'application/json' },
1648
+ body: JSON.stringify({ path: rel, newName: String(nn).trim() })
1649
+ }).then(function (r) { return r.json(); }).then(function (d) {
1650
+ if (d && d.ok) { location.reload(); }
1651
+ else { showMsg('重命名失败:' + ((d && d.error) || '未知'), 'error'); }
1652
+ }).catch(function (err) { showMsg('重命名失败:' + err, 'error'); });
1653
+ } else if (c) {
1654
+ e.preventDefault(); e.stopPropagation();
1655
+ var rel2 = c.getAttribute('data-copypath');
1656
+ try {
1657
+ navigator.clipboard.writeText(rel2).then(function () { showMsg('已复制路径:' + rel2); }, function () { showMsg('复制失败', 'error'); });
1658
+ } catch (e2) { showMsg('复制失败', 'error'); }
1659
+ }
1660
+ });
1661
+
1662
+ // 工作区概览(仪表盘):从各文件行的 mtime/size 统计 今日/7日/全部/总大小。
1663
+ (function () {
1664
+ var rows = Array.prototype.slice.call(document.querySelectorAll('tr[data-ts]'));
1665
+ var now = Date.now();
1666
+ var today = 0, week = 0, size = 0;
1667
+ rows.forEach(function (tr) {
1668
+ var ts = Number(tr.dataset.ts) || 0;
1669
+ if (tr.dataset.date === todayStr) today++;
1670
+ if (now - ts <= 7 * 86400000) week++;
1671
+ size += Number(tr.dataset.size) || 0;
1672
+ });
1673
+ function setTxt(id, v) { var el = document.getElementById(id); if (el) el.textContent = v; }
1674
+ setTxt('dashTodayN', today);
1675
+ setTxt('dashWeekN', week);
1676
+ setTxt('dashTotalN', rows.length);
1677
+ var radial = document.getElementById('radial');
1678
+ var radialBtn = document.getElementById('radialBtn');
1679
+ var radToday = document.getElementById('radialToday');
1680
+ var radWeek = document.getElementById('radialWeek');
1681
+ var radAll = document.getElementById('radialAll');
1682
+ if (radialBtn) radialBtn.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); if (radial) radial.classList.toggle('expanded'); });
1683
+ function syncDashActive() {
1684
+ var sel = dayInput.value;
1685
+ if (radToday) radToday.classList.toggle('on', !weekOn && sel === todayStr);
1686
+ if (radWeek) radWeek.classList.toggle('on', weekOn);
1687
+ if (radAll) radAll.classList.toggle('on', !weekOn && !sel);
1688
+ }
1689
+ if (radToday) radToday.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); weekOn = false; dayInput.value = todayStr; apply(); syncDashActive(); });
1690
+ if (radWeek) radWeek.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); weekOn = true; dayInput.value = ''; apply(); syncDashActive(); });
1691
+ if (radAll) radAll.addEventListener('click', function (e) { e.preventDefault(); e.stopPropagation(); weekOn = false; dayInput.value = ''; apply(); syncDashActive(); });
1692
+ syncDashActive();
1693
+ })();
1545
1694
  })();
1546
1695
  </script>
1547
1696
  </body>
@@ -1874,7 +2023,7 @@ export async function apply(ctx, config = {}) {
1874
2023
  const skillsRoot = resolve(config.skillsRoot ?? DEFAULT_SKILLS_ROOT);
1875
2024
  const onError = (error) => ctx.logger.error(error instanceof Error ? error : new Error(String(error)));
1876
2025
 
1877
- const handlers = createHandlers({ trustedHosts, onError });
2026
+ const handlers = createHandlers({ trustedHosts, onError, excludedWorkspaceNames: config.excludedWorkspaceNames });
1878
2027
 
1879
2028
  // ---- md2docx 工具: Markdown → Word (.docx, 带页码) ----
1880
2029
  // 调用 md2docx.py (pandoc-free, python-docx + 页脚 PAGE 字段)。
@@ -2043,6 +2192,12 @@ export async function apply(ctx, config = {}) {
2043
2192
  handler: handlers.workspaceSave,
2044
2193
  }), "dsh-long-plugins: workspace save route");
2045
2194
 
2195
+ ctx.effect(() => ctx.webServer.register({
2196
+ kind: "exact",
2197
+ path: "/api/dsh-uploads/workspace-file/rename",
2198
+ handler: handlers.workspaceRename,
2199
+ }), "dsh-long-plugins: workspace rename route");
2200
+
2046
2201
  // ---- 技能文档 (skill docs) routes ----
2047
2202
  ctx.effect(() => ctx.webServer.register({
2048
2203
  kind: "exact",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-long-plugins",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
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",