dsh-long-plugins 1.3.8 → 1.3.10
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 +14 -1
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/client/client.js +53 -7
- package/client/vendor/xlsx.full.min.js +22 -0
- package/dsh.plugin.json +1 -1
- package/lib/index.js +407 -29
- package/package.json +1 -1
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.
|
|
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";
|
|
@@ -503,36 +503,95 @@ async function docxHtml(buf) {
|
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
-
/**
|
|
506
|
+
/** Excel 列号(A=1, AA=27)。 */
|
|
507
|
+
function colToNum(letters) {
|
|
508
|
+
let n = 0;
|
|
509
|
+
for (const ch of String(letters).toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64);
|
|
510
|
+
return n;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** 渲染 .xlsx 为 HTML 表格(多工作表 + 合并单元格 + 表头样式,bound 保持预览量)。 */
|
|
507
514
|
async function xlsxHtml(buf) {
|
|
508
515
|
try {
|
|
509
516
|
const workbook = new ExcelJS.Workbook();
|
|
510
517
|
await workbook.xlsx.load(buf);
|
|
511
|
-
const
|
|
512
|
-
if (
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
518
|
+
const sheets = workbook.worksheets.slice(0, 3);
|
|
519
|
+
if (sheets.length === 0) return "<p>(空工作簿)</p>";
|
|
520
|
+
let html = "";
|
|
521
|
+
for (const sheet of sheets) {
|
|
522
|
+
// 合并范围:topLeft -> {rowspan, colspan};并记录被覆盖的格子。
|
|
523
|
+
// exceljs 版本差异:mergedCells 可能是 getter;用 model.merges 兜底(格式 "A1:C1")。
|
|
524
|
+
const merges = (sheet.mergedCells && sheet.mergedCells.length ? sheet.mergedCells : (sheet.model && sheet.model.merges)) || [];
|
|
525
|
+
const merged = new Map();
|
|
526
|
+
const covered = new Set();
|
|
527
|
+
for (const range of merges) {
|
|
528
|
+
const m = /^([A-Z]+)(\d+):([A-Z]+)(\d+)$/.exec(String(range));
|
|
529
|
+
if (!m) continue;
|
|
530
|
+
const c1 = colToNum(m[1]), r1 = parseInt(m[2], 10), c2 = colToNum(m[3]), r2 = parseInt(m[4], 10);
|
|
531
|
+
merged.set(`${r1},${c1}`, { rowspan: r2 - r1 + 1, colspan: c2 - c1 + 1 });
|
|
532
|
+
for (let rr = r1; rr <= r2; rr++) for (let cc = c1; cc <= c2; cc++) if (!(rr === r1 && cc === c1)) covered.add(`${rr},${cc}`);
|
|
533
|
+
}
|
|
534
|
+
const rowLimit = 500, colLimit = 32;
|
|
535
|
+
const rows = [];
|
|
536
|
+
sheet.eachRow({ includeEmpty: false }, (row, rowNumber) => {
|
|
537
|
+
if (rows.length >= rowLimit) return;
|
|
538
|
+
const rn = rowNumber;
|
|
539
|
+
const cells = [];
|
|
540
|
+
for (let col = 1; col <= Math.min(row.cellCount, colLimit); col += 1) {
|
|
541
|
+
if (covered.has(`${rn},${col}`)) continue;
|
|
542
|
+
let value = row.getCell(col).value;
|
|
543
|
+
if (value !== null && typeof value === "object") {
|
|
544
|
+
if (value.richText) value = value.richText.map((t) => t.text).join("");
|
|
545
|
+
else if (value.text !== undefined) value = value.text;
|
|
546
|
+
else if (value.result !== undefined) value = value.result;
|
|
547
|
+
else value = "";
|
|
548
|
+
}
|
|
549
|
+
const span = merged.get(`${rn},${col}`);
|
|
550
|
+
const attr = span ? (span.colspan > 1 ? ` colspan="${span.colspan}"` : "") + (span.rowspan > 1 ? ` rowspan="${span.rowspan}"` : "") : "";
|
|
551
|
+
cells.push(`<td${attr}>${escHtml(value ?? "")}</td>`);
|
|
527
552
|
}
|
|
528
|
-
|
|
553
|
+
rows.push(`<tr>${cells.join("")}</tr>`);
|
|
554
|
+
});
|
|
555
|
+
const header = rows.length > 0 ? `<thead>${rows[0].replace(/<td/g, "<th").replace(/<\/td>/g, "</th>")}</thead>` : "";
|
|
556
|
+
const body = rows.length > 1 ? `<tbody>${rows.slice(1).join("")}</tbody>` : "";
|
|
557
|
+
html += `<div class="sheet"><h3>${escHtml(sheet.name || "Sheet")}</h3><table>${header}${body}</table></div>`;
|
|
558
|
+
}
|
|
559
|
+
const css = "<style>table{border-collapse:collapse;width:100%;font-size:12px;margin-bottom:14px}th,td{border:1px solid #cfd5dd;padding:4px 8px;white-space:pre-wrap;word-break:break-all;vertical-align:top}thead th{background:#f0f3f8;font-weight:600;text-align:left}.sheet h3{font-size:14px;margin:8px 0 4px;color:#1f3a5f}</style>";
|
|
560
|
+
return `${css}${html}`;
|
|
561
|
+
} catch {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** 解析 CSV/TSV 文本为 HTML 表格(首行当作表头,bound)。 */
|
|
567
|
+
function csvHtml(text) {
|
|
568
|
+
try {
|
|
569
|
+
const limit = 500;
|
|
570
|
+
const rows = [];
|
|
571
|
+
const parse = (line) => {
|
|
572
|
+
const out = [];
|
|
573
|
+
let cur = "", inQ = false;
|
|
574
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
575
|
+
const ch = line[i];
|
|
576
|
+
if (inQ) {
|
|
577
|
+
if (ch === '"') { if (line[i + 1] === '"') { cur += '"'; i += 1; } else inQ = false; }
|
|
578
|
+
else cur += ch;
|
|
579
|
+
} else if (ch === '"') { inQ = true; }
|
|
580
|
+
else if (ch === "," || ch === "\t") { out.push(cur); cur = ""; }
|
|
581
|
+
else cur += ch;
|
|
529
582
|
}
|
|
530
|
-
|
|
531
|
-
|
|
583
|
+
out.push(cur);
|
|
584
|
+
return out;
|
|
585
|
+
};
|
|
586
|
+
for (const line of String(text || "").split(/\r?\n/)) {
|
|
587
|
+
if (rows.length >= limit) break;
|
|
588
|
+
if (line.trim() === "") continue;
|
|
589
|
+
rows.push(parse(line));
|
|
532
590
|
}
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
const
|
|
591
|
+
if (rows.length === 0) return "<p>(空)</p>";
|
|
592
|
+
const header = `<thead><tr>${rows[0].map((c) => `<th>${escHtml(c)}</th>`).join("")}</tr></thead>`;
|
|
593
|
+
const body = rows.length > 1 ? `<tbody>${rows.slice(1).map((r) => `<tr>${r.map((c) => `<td>${escHtml(c)}</td>`).join("")}</tr>`).join("")}</tbody>` : "";
|
|
594
|
+
const css = "<style>table{border-collapse:collapse;width:100%;font-size:12px}th,td{border:1px solid #cfd5dd;padding:4px 8px;white-space:pre-wrap;word-break:break-all;vertical-align:top}thead th{background:#f0f3f8;font-weight:600;text-align:left}</style>";
|
|
536
595
|
return `${css}<table>${header}${body}</table>`;
|
|
537
596
|
} catch {
|
|
538
597
|
return null;
|
|
@@ -603,6 +662,7 @@ async function officePreviewHtml(name, buffer) {
|
|
|
603
662
|
if (ext === ".docx") return docxHtml(buffer);
|
|
604
663
|
if (ext === ".xlsx") return xlsxHtml(buffer);
|
|
605
664
|
if (ext === ".pptx") return pptxHtml(buffer);
|
|
665
|
+
if (ext === ".csv" || ext === ".tsv") return csvHtml(buffer.toString("utf8"));
|
|
606
666
|
return null;
|
|
607
667
|
}
|
|
608
668
|
|
|
@@ -819,9 +879,11 @@ export function createHandlers(options = {}) {
|
|
|
819
879
|
const buffer = await readFile(full);
|
|
820
880
|
const binary = buffer.subarray(0, 8192).includes(0);
|
|
821
881
|
const truncated = buffer.length > PREVIEW_LIMIT;
|
|
822
|
-
|
|
882
|
+
// Office 二进制(docx/xlsx/pptx)或 CSV/TSV 文本 → 表格 HTML 预览
|
|
883
|
+
const tableHtml = (binary && OFFICE_EXTS.has(extname(name).toLowerCase())) || /\.(csv|tsv)$/i.test(name)
|
|
823
884
|
? await officePreviewHtml(name, buffer)
|
|
824
885
|
: undefined;
|
|
886
|
+
const officeHtml = tableHtml;
|
|
825
887
|
// Markdown 返回内联样式后的完整 HTML 片段(含 MD_CSS),供前端 srcDoc 渲染真实效果,
|
|
826
888
|
// 避免内嵌带独立头部的 workspace-preview 页面导致按钮重复、放大失效。
|
|
827
889
|
const mdHtml = !binary && /\.(md|markdown)$/i.test(name)
|
|
@@ -865,6 +927,34 @@ export function createHandlers(options = {}) {
|
|
|
865
927
|
}
|
|
866
928
|
};
|
|
867
929
|
|
|
930
|
+
// 重命名工作区文件:只改文件名(限定同目录,不跨目录移动),低风险。
|
|
931
|
+
const workspaceRename = async (req, res) => {
|
|
932
|
+
try {
|
|
933
|
+
requireTrusted(req);
|
|
934
|
+
if (req.method !== "POST") {
|
|
935
|
+
methodNotAllowed(res, ["POST"]);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const body = await readJsonBody(req);
|
|
939
|
+
const rel = typeof body === "object" && body !== null ? body.path : undefined;
|
|
940
|
+
const newName = typeof body === "object" && body !== null ? body.newName : undefined;
|
|
941
|
+
const full = safeResolve(workspaceRoot, rel);
|
|
942
|
+
if (full === undefined) throw new HttpError(400, "invalid path");
|
|
943
|
+
const info = await stat(full);
|
|
944
|
+
if (!info.isFile()) throw new HttpError(400, "not a regular file");
|
|
945
|
+
// 新文件名必须是安全的纯文件名(无分隔符/路径穿越/隐藏/特殊)
|
|
946
|
+
if (!isSafeStoredName(newName)) throw new HttpError(400, "invalid name");
|
|
947
|
+
const newRel = join(dirname(rel), newName);
|
|
948
|
+
const newFull = safeResolve(workspaceRoot, newRel);
|
|
949
|
+
if (newFull === undefined || newFull === full) throw new HttpError(400, "invalid target");
|
|
950
|
+
// 同目录纯改名:rename 原子且安全(不跨目录移动)
|
|
951
|
+
await rename(full, newFull);
|
|
952
|
+
sendJson(res, 200, { ok: true, path: newRel });
|
|
953
|
+
} catch (error) {
|
|
954
|
+
sendError(res, error, onError);
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
|
|
868
958
|
const workspaceSave = async (req, res) => {
|
|
869
959
|
try {
|
|
870
960
|
requireTrusted(req);
|
|
@@ -1242,7 +1332,229 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
|
|
|
1242
1332
|
}
|
|
1243
1333
|
};
|
|
1244
1334
|
|
|
1245
|
-
|
|
1335
|
+
// ---- xlsx 真实预览(浏览器端渲染,所见即所得)----
|
|
1336
|
+
// 返回一个自包含 HTML:引 SheetJS(xlsx.full.min.js) 解析 .xlsx,转换成 x-spreadsheet
|
|
1337
|
+
// 的 data 结构,用 x-spreadsheet 渲染成真实电子表格网格(行列表头/合并/冻结/缩放)。
|
|
1338
|
+
// 仅供参考;纯浏览器端渲染,不依赖 NAS 端转换,效果与 Excel/浏览器一致。
|
|
1339
|
+
const xlsxPreviewPage = async (req, res) => {
|
|
1340
|
+
try {
|
|
1341
|
+
requireTrusted(req);
|
|
1342
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1343
|
+
methodNotAllowed(res, ["GET", "HEAD"]);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const rel = (() => {
|
|
1347
|
+
try {
|
|
1348
|
+
return decodeURIComponent(new URL(req.url || "/", "http://dsh.internal").searchParams.get("path") || "");
|
|
1349
|
+
} catch {
|
|
1350
|
+
return "";
|
|
1351
|
+
}
|
|
1352
|
+
})();
|
|
1353
|
+
// 只允许 .xlsx(避免把别的文件喂给 SheetJS/x-spreadsheet)
|
|
1354
|
+
if (!/\.xlsx$/i.test(rel)) throw new HttpError(400, "only .xlsx supported");
|
|
1355
|
+
const full = safeResolve(workspaceRoot, rel);
|
|
1356
|
+
if (full === undefined) throw new HttpError(400, "invalid path");
|
|
1357
|
+
const info = await stat(full);
|
|
1358
|
+
if (!info.isFile()) throw new HttpError(400, "not a regular file");
|
|
1359
|
+
const name = rel.split("/").pop() || "file";
|
|
1360
|
+
const downloadHref = `workspace-file?path=${encodeURIComponent(rel)}&download=1`;
|
|
1361
|
+
const inlineHref = `workspace-file?path=${encodeURIComponent(rel)}&inline=1`;
|
|
1362
|
+
const assetBase = "/api/dsh-uploads/xlsx-preview-asset";
|
|
1363
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
1364
|
+
res.end(`<!DOCTYPE html>
|
|
1365
|
+
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1366
|
+
<title>预览 - ${escapeHtml(name)}</title>
|
|
1367
|
+
<style>
|
|
1368
|
+
html,body{margin:0;height:100%;background:#1a2530;color:#e5e7eb;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}
|
|
1369
|
+
.bar{position:sticky;top:0;z-index:20;display:flex;gap:8px;padding:10px 14px;align-items:center;background:#1a2530;border-bottom:1px solid #2c3a47;flex-wrap:wrap}
|
|
1370
|
+
.bar strong{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e5e7eb;font-size:13px}
|
|
1371
|
+
.bar button,.bar a{padding:6px 12px;border:1px solid #2c3a47;border-radius:8px;background:transparent;color:#e5e7eb;text-decoration:none;font-size:13px;cursor:pointer;white-space:nowrap}
|
|
1372
|
+
.tabs{display:flex;gap:6px;padding:8px 14px;background:#141f2b;border-bottom:1px solid #2c3a47;overflow:auto}
|
|
1373
|
+
.tabs button{padding:4px 12px;border:1px solid #2c3a47;border-radius:7px;background:transparent;color:#9ca3af;font-size:12px;cursor:pointer;white-space:nowrap;flex:none}
|
|
1374
|
+
.tabs button.on{background:#2563eb;color:#fff;border-color:#2563eb}
|
|
1375
|
+
// 静态图:整表一次性画到 canvas。拖动=移动/滚动一张画好的图,不重绘;放大用 CSS width/height 变化(与 ppt 预览一致)。
|
|
1376
|
+
#stage{position:relative;overflow:auto;height:calc(100vh - 48px);background:#e9ebee;padding:18px}
|
|
1377
|
+
#wrap{display:block;width:max-content;margin:0 auto;background:#fff;box-shadow:0 2px 14px rgba(0,0,0,.18)}
|
|
1378
|
+
canvas{display:block}
|
|
1379
|
+
#msg{color:#9ca3af;padding:24px;text-align:center;font-size:14px;font-family:inherit}
|
|
1380
|
+
</style>
|
|
1381
|
+
</head><body>
|
|
1382
|
+
<div class="bar">
|
|
1383
|
+
<strong>${escapeHtml(name)}</strong>
|
|
1384
|
+
<span style="display:flex;gap:4px;align-items:center">
|
|
1385
|
+
<button type="button" title="缩小" onclick="zoomBy(0.8)">−</button>
|
|
1386
|
+
<button type="button" title="放大" onclick="zoomBy(1.25)">+</button>
|
|
1387
|
+
<button type="button" title="适合宽度" onclick="zoomFit()">适合</button>
|
|
1388
|
+
</span>
|
|
1389
|
+
<a href="${downloadHref}" download>下载</a>
|
|
1390
|
+
<button type="button" onclick="closePreview()">✕ 关闭</button>
|
|
1391
|
+
</div>
|
|
1392
|
+
<div class="tabs" id="tabs"></div>
|
|
1393
|
+
<div id="stage"><div id="wrap"><canvas id="canvas"></canvas><div id="msg">加载中…</div></div></div>
|
|
1394
|
+
<script src="${assetBase}?f=xlsx.full.min.js"></script>
|
|
1395
|
+
<script>
|
|
1396
|
+
function closePreview(){
|
|
1397
|
+
try{ if(window.self!==window.top&&window.parent){ window.parent.postMessage({type:'dsh-close-preview'},location.origin); return; } }catch(e){ /* 跨域忽略 */ }
|
|
1398
|
+
window.close();
|
|
1399
|
+
}
|
|
1400
|
+
const stage=document.getElementById('stage');
|
|
1401
|
+
const canvas=document.getElementById('canvas');
|
|
1402
|
+
const wrap=document.getElementById('wrap');
|
|
1403
|
+
const msg=document.getElementById('msg');
|
|
1404
|
+
const tabsEl=document.getElementById('tabs');
|
|
1405
|
+
const dpr=Math.min(window.devicePixelRatio||1, 2);
|
|
1406
|
+
const HDR=26, IDXW=48, ROWH=26;
|
|
1407
|
+
const FONT='13px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif';
|
|
1408
|
+
const FONTB='bold 13px -apple-system,"PingFang SC","Microsoft YaHei",sans-serif';
|
|
1409
|
+
var zoom=1, fitMode=true, natW=800, natH=600, wb=null, sheetIdx=0;
|
|
1410
|
+
function fmt(v){
|
|
1411
|
+
if(v===null||v===undefined) return '';
|
|
1412
|
+
if(v instanceof Date){ const p=(n)=>String(n).padStart(2,'0'); return v.getFullYear()+'-'+p(v.getMonth()+1)+'-'+p(v.getDate()); }
|
|
1413
|
+
return String(v);
|
|
1414
|
+
}
|
|
1415
|
+
function colLetter(c){ c+=1; var s=''; while(c>0){ var m=(c-1)%26; s=String.fromCharCode(65+m)+s; c=(c-m-1)/26; } return s; }
|
|
1416
|
+
function applyZoom(){
|
|
1417
|
+
canvas.style.width=Math.round(natW*zoom)+'px';
|
|
1418
|
+
canvas.style.height=Math.round(natH*zoom)+'px';
|
|
1419
|
+
}
|
|
1420
|
+
function zoomFit(){
|
|
1421
|
+
zoom=Math.min(1,((stage.clientWidth-36)||600)/natW); fitMode=true; applyZoom();
|
|
1422
|
+
}
|
|
1423
|
+
function zoom100(){ zoom=1; fitMode=false; applyZoom(); }
|
|
1424
|
+
function zoomBy(f){ zoom=Math.min(8,Math.max(0.05,zoom*f)); fitMode=false; applyZoom(); }
|
|
1425
|
+
function renderTabs(){
|
|
1426
|
+
if(!wb) return;
|
|
1427
|
+
tabsEl.innerHTML='';
|
|
1428
|
+
wb.SheetNames.forEach(function(nm,i){
|
|
1429
|
+
var b=document.createElement('button'); b.textContent=nm;
|
|
1430
|
+
if(i===sheetIdx) b.className='on';
|
|
1431
|
+
b.onclick=function(){ sheetIdx=i; renderSheet(); };
|
|
1432
|
+
tabsEl.appendChild(b);
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
function renderSheet(){
|
|
1436
|
+
msg.style.display='';
|
|
1437
|
+
var ws=wb.Sheets[wb.SheetNames[sheetIdx]];
|
|
1438
|
+
var range=ws['!ref']?XLSX.utils.decode_range(ws['!ref']):{s:{r:0,c:0},e:{r:0,c:0}};
|
|
1439
|
+
var mc=document.createElement('canvas').getContext('2d'); mc.font=FONT;
|
|
1440
|
+
var texts={}, maxCols={};
|
|
1441
|
+
for(var r=range.s.r;r<=range.e.r;r++){
|
|
1442
|
+
for(var c=range.s.c;c<=range.e.c;c++){
|
|
1443
|
+
var cell=ws[XLSX.utils.encode_cell({r:r,c:c})];
|
|
1444
|
+
var t='';
|
|
1445
|
+
if(cell){ if(cell.t==='n')t=String(cell.v); else if(cell.t==='b')t=cell.v?'TRUE':'FALSE'; else t=fmt(cell.v); }
|
|
1446
|
+
texts[r+'_'+c]=t;
|
|
1447
|
+
if(t){ var w=mc.measureText(t).width+18; if(!maxCols[c]||w>maxCols[c])maxCols[c]=w; }
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
var colW={};
|
|
1451
|
+
for(var c=range.s.c;c<=range.e.c;c++){ colW[c]=Math.max(64,Math.min(380,maxCols[c]||120)); }
|
|
1452
|
+
var colX={}, acc=IDXW;
|
|
1453
|
+
for(var c=range.s.c;c<=range.e.c;c++){ colX[c]=acc; acc+=colW[c]; }
|
|
1454
|
+
var rowY={}, accy=HDR;
|
|
1455
|
+
for(var r=range.s.r;r<=range.e.r;r++){ rowY[r]=accy; accy+=ROWH; }
|
|
1456
|
+
// 合并单元格:记录左上角跨的格数,其余标记为 covered(不画独立边框/文字)
|
|
1457
|
+
var mergeMap={}, covered={};
|
|
1458
|
+
(ws['!merges']||[]).forEach(function(m){
|
|
1459
|
+
mergeMap[m.s.r+'_'+m.s.c]={rs:m.e.r-m.s.r+1, cs:m.e.c-m.s.c+1};
|
|
1460
|
+
for(var rr=m.s.r;rr<=m.e.r;rr++)for(var cc=m.s.c;cc<=m.e.c;cc++){ if(rr===m.s.r&&cc===m.s.c)continue; covered[rr+'_'+cc]=1; }
|
|
1461
|
+
});
|
|
1462
|
+
var totalW=acc, totalH=accy; natW=totalW; natH=totalH;
|
|
1463
|
+
var w=Math.round(totalW*dpr), h=Math.round(totalH*dpr);
|
|
1464
|
+
var maxDim=16000;
|
|
1465
|
+
if(w>maxDim||h>maxDim){ var s=Math.min(maxDim/Math.max(1,w),maxDim/Math.max(1,h)); w=Math.round(totalW*dpr*s); h=Math.round(totalH*dpr*s); }
|
|
1466
|
+
canvas.width=w; canvas.height=h;
|
|
1467
|
+
var ctx=canvas.getContext('2d');
|
|
1468
|
+
ctx.setTransform(w/Math.max(1,totalW),0,0,h/Math.max(1,totalH),0,0);
|
|
1469
|
+
ctx.textBaseline='middle';
|
|
1470
|
+
ctx.fillStyle='#ffffff'; ctx.fillRect(0,0,totalW,totalH);
|
|
1471
|
+
// 左上角
|
|
1472
|
+
ctx.fillStyle='#e9ebee'; ctx.fillRect(0,0,IDXW,HDR);
|
|
1473
|
+
// 列标头
|
|
1474
|
+
ctx.font=FONTB; ctx.fillStyle='#374151';
|
|
1475
|
+
for(var c=range.s.c;c<=range.e.c;c++){
|
|
1476
|
+
ctx.fillStyle='#f3f4f7'; ctx.fillRect(colX[c],0,colW[c],HDR);
|
|
1477
|
+
ctx.strokeStyle='#dfe3ea'; ctx.beginPath(); ctx.moveTo(colX[c],0); ctx.lineTo(colX[c],HDR); ctx.stroke();
|
|
1478
|
+
ctx.beginPath(); ctx.moveTo(colX[c],HDR); ctx.lineTo(colX[c]+colW[c],HDR); ctx.stroke();
|
|
1479
|
+
var lx=colX[c]+colW[c]/2;
|
|
1480
|
+
ctx.fillStyle='#374151'; ctx.fillText(colLetter(c),lx,HDR/2+0.5);
|
|
1481
|
+
}
|
|
1482
|
+
// 行标头
|
|
1483
|
+
for(var r=range.s.r;r<=range.e.r;r++){
|
|
1484
|
+
ctx.fillStyle='#f3f4f7'; ctx.fillRect(0,rowY[r],IDXW,ROWH);
|
|
1485
|
+
ctx.strokeStyle='#dfe3ea'; ctx.beginPath(); ctx.moveTo(IDXW,rowY[r]); ctx.lineTo(IDXW,rowY[r]+ROWH); ctx.stroke();
|
|
1486
|
+
ctx.beginPath(); ctx.moveTo(0,rowY[r]+ROWH); ctx.lineTo(IDXW,rowY[r]+ROWH); ctx.stroke();
|
|
1487
|
+
ctx.fillStyle='#374151'; ctx.fillText(String(r+1),IDXW/2,rowY[r]+ROWH/2+0.5);
|
|
1488
|
+
}
|
|
1489
|
+
// 单元格
|
|
1490
|
+
ctx.font=FONT;
|
|
1491
|
+
for(var r=range.s.r;r<=range.e.r;r++){
|
|
1492
|
+
for(var c=range.s.c;c<=range.e.c;c++){
|
|
1493
|
+
var key=r+'_'+c;
|
|
1494
|
+
if(covered[key]) continue;
|
|
1495
|
+
var x=colX[c], y=rowY[r], ww=colW[c], hh=ROWH;
|
|
1496
|
+
var mk=mergeMap[key];
|
|
1497
|
+
if(mk){ for(var i=1;i<mk.cs;i++)ww+=colW[c+i]; for(var j=1;j<mk.rs;j++)hh+=ROWH; }
|
|
1498
|
+
ctx.fillStyle='#ffffff'; ctx.fillRect(x,y,ww,hh);
|
|
1499
|
+
ctx.strokeStyle='#e2e5ea'; ctx.strokeRect(x+0.5,y+0.5,ww-1,hh-1);
|
|
1500
|
+
var t=texts[key];
|
|
1501
|
+
if(t){
|
|
1502
|
+
var isNum=(ws[XLSX.utils.encode_cell({r:r,c:c})]||{}).t==='n';
|
|
1503
|
+
ctx.fillStyle='#1f2937';
|
|
1504
|
+
var xoff=isNum?(x+ww-6):(x+6);
|
|
1505
|
+
ctx.textAlign=isNum?'right':'left';
|
|
1506
|
+
ctx.fillText(t,xoff,y+hh/2+0.5);
|
|
1507
|
+
ctx.textAlign='left';
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
msg.style.display='none';
|
|
1512
|
+
renderTabs();
|
|
1513
|
+
zoomFit();
|
|
1514
|
+
}
|
|
1515
|
+
(async()=>{
|
|
1516
|
+
try{
|
|
1517
|
+
const r=await fetch(${JSON.stringify(inlineHref)},{cache:'no-store'});
|
|
1518
|
+
if(!r.ok) throw new Error('HTTP '+r.status);
|
|
1519
|
+
const buf=await r.arrayBuffer();
|
|
1520
|
+
wb=XLSX.read(new Uint8Array(buf),{type:'array',cellDates:true});
|
|
1521
|
+
renderSheet();
|
|
1522
|
+
}catch(e){ msg.style.display=''; msg.textContent='渲染失败:'+((e&&e.message)||e); }
|
|
1523
|
+
})();
|
|
1524
|
+
window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else applyZoom(); }catch(e){} });
|
|
1525
|
+
</script>
|
|
1526
|
+
|
|
1527
|
+
</body></html>`);
|
|
1528
|
+
} catch (error) {
|
|
1529
|
+
sendError(res, error, onError);
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
// serve SheetJS 前端库(从 PACKAGE_DIR/client/vendor 读)
|
|
1534
|
+
const xlsxPreviewAsset = async (req, res) => {
|
|
1535
|
+
try {
|
|
1536
|
+
requireTrusted(req);
|
|
1537
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1538
|
+
methodNotAllowed(res, ["GET", "HEAD"]);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const f = decodeURIComponent(new URL(req.url || "/", "http://dsh.internal").searchParams.get("f") || "");
|
|
1542
|
+
// 只允许 xlsx 预览页用到的 SheetJS 解析库(浏览器端把 .xlsx 转成静态表格图)。
|
|
1543
|
+
if (!["xlsx.full.min.js"].includes(f)) throw new HttpError(400, "unknown asset");
|
|
1544
|
+
const target = join(VENDOR_DIR, f);
|
|
1545
|
+
const buf = await readFile(target);
|
|
1546
|
+
res.writeHead(200, {
|
|
1547
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1548
|
+
"cache-control": "no-store",
|
|
1549
|
+
"x-content-type-options": "nosniff",
|
|
1550
|
+
});
|
|
1551
|
+
res.end(buf);
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
sendError(res, error, onError);
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
|
|
1557
|
+
return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset };
|
|
1246
1558
|
}
|
|
1247
1559
|
|
|
1248
1560
|
/** 人类可读文件大小。 */
|
|
@@ -1296,19 +1608,23 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
|
|
|
1296
1608
|
const isPdf = /\.pdf$/i.test(f.name);
|
|
1297
1609
|
const isDocx = /\.docx$/i.test(f.name);
|
|
1298
1610
|
const isPptx = /\.pptx$/i.test(f.name);
|
|
1611
|
+
const isXlsx = /\.xlsx$/i.test(f.name);
|
|
1299
1612
|
// PDF 直接嵌原始流(单层 iframe,浏览器原生查看器可滚动翻页);
|
|
1300
|
-
// docx 走 docx-preview、pptx 走 PptxViewJS
|
|
1613
|
+
// docx 走 docx-preview、pptx 走 PptxViewJS、xlsx 走 x-spreadsheet 真实渲染页
|
|
1614
|
+
// (浏览器端解析,所见即所得);
|
|
1301
1615
|
// 其它可预览类型走渲染页;不可预览 → 下载。
|
|
1302
1616
|
const viewHref = previewable
|
|
1303
|
-
? (isPdf ? `workspace-file?path=${rel}&inline=1` : isDocx ? `docx-preview?path=${rel}` : isPptx ? `pptx-preview?path=${rel}` : `workspace-preview?path=${rel}&from=list`)
|
|
1617
|
+
? (isPdf ? `workspace-file?path=${rel}&inline=1` : isDocx ? `docx-preview?path=${rel}` : isPptx ? `pptx-preview?path=${rel}` : isXlsx ? `xlsx-preview?path=${rel}` : `workspace-preview?path=${rel}&from=list`)
|
|
1304
1618
|
: `workspace-file?path=${rel}&download=1`;
|
|
1305
1619
|
return `<tr data-ts="${Math.floor(f.mtime)}" data-name="${escapeHtml(f.name)}" data-size="${f.size}">
|
|
1306
1620
|
<td class="name"><a class="flink" href="${viewHref}" data-preview="${escapeHtml(viewHref)}" title="${escapeHtml(f.path)}">${escapeHtml(f.name)}</a></td>
|
|
1307
1621
|
<td class="size">${humanSize(f.size)}</td>
|
|
1308
1622
|
<td class="time">${humanTime(f.mtime)}</td>
|
|
1309
1623
|
<td class="acts">
|
|
1310
|
-
|
|
1624
|
+
<button type="button" class="tag plain" data-copypath="${escapeHtml(f.path)}">复制路径</button>
|
|
1625
|
+
<button type="button" class="tag plain" data-rename="${escapeHtml(f.path)}">重命名</button>
|
|
1311
1626
|
<a class="tag dl" href="workspace-file?path=${rel}&download=1">${ICON_DL} 下载</a>
|
|
1627
|
+
${previewable ? `<a class="tag" href="${viewHref}" data-preview="${escapeHtml(viewHref)}">${ICON_EYE} 预览</a>` : ""}
|
|
1312
1628
|
</td>
|
|
1313
1629
|
</tr>`;
|
|
1314
1630
|
}).join("");
|
|
@@ -1392,6 +1708,9 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
|
|
|
1392
1708
|
.fsearch:focus { outline:none; border-color:var(--lp-accent); }
|
|
1393
1709
|
.fsearch-clear { background:transparent; border:none; color:var(--lp-meta); font-size:13px; line-height:1; cursor:pointer; padding:0 3px; }
|
|
1394
1710
|
.fsearch-clear:hover { color:var(--lp-fg); }
|
|
1711
|
+
.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); }
|
|
1712
|
+
.wsb-toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
|
|
1713
|
+
.wsb-toast.error { border-color:#e25050; color:#ffb4b4; }
|
|
1395
1714
|
.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; }
|
|
1396
1715
|
.btn2:hover { background:var(--lp-btn-hover); }
|
|
1397
1716
|
.daybtn.active { background:#2563eb; color:#fff; }
|
|
@@ -1408,6 +1727,9 @@ function workspaceBrowseHtml(groups, ws = "", all = false) {
|
|
|
1408
1727
|
.tag { display:inline-flex; align-items:center; gap:4px; text-decoration:none; font-size:12px; padding:4px 10px; border-radius:6px; margin-right:6px; background:var(--lp-tag-bg); color:var(--lp-accent); }
|
|
1409
1728
|
.tag:hover { background:var(--lp-tag-hover); }
|
|
1410
1729
|
.tag.dl { color:var(--lp-ok); }
|
|
1730
|
+
/* 重命名/复制路径:不设强调色,做白色/常规文字按钮 */
|
|
1731
|
+
.tag.plain { background:transparent; color:var(--lp-fg); border:1px solid var(--lp-border); }
|
|
1732
|
+
.tag.plain:hover { background:var(--lp-hover); }
|
|
1411
1733
|
.empty { color:var(--lp-meta); text-align:center; padding:60px 0; }
|
|
1412
1734
|
.ic { width:14px; height:14px; flex:none; }
|
|
1413
1735
|
@media (max-width: 640px) {
|
|
@@ -1588,6 +1910,44 @@ document.addEventListener('click', function (e) {
|
|
|
1588
1910
|
if (savedOpen) setFsearchOpen(true);
|
|
1589
1911
|
apply();
|
|
1590
1912
|
|
|
1913
|
+
// 重命名 / 复制路径
|
|
1914
|
+
function showMsg(text, kind) {
|
|
1915
|
+
var t = document.querySelector('.wsb-toast');
|
|
1916
|
+
if (!t) {
|
|
1917
|
+
t = document.createElement('div');
|
|
1918
|
+
t.className = 'wsb-toast';
|
|
1919
|
+
document.body.appendChild(t);
|
|
1920
|
+
}
|
|
1921
|
+
t.textContent = text;
|
|
1922
|
+
t.className = 'wsb-toast show' + (kind ? ' ' + kind : '');
|
|
1923
|
+
clearTimeout(t._t);
|
|
1924
|
+
t._t = setTimeout(function () { t.className = 'wsb-toast'; }, 2600);
|
|
1925
|
+
}
|
|
1926
|
+
document.addEventListener('click', function (e) {
|
|
1927
|
+
var t = e.target && e.target.closest ? e.target.closest('[data-rename]') : null;
|
|
1928
|
+
var c = e.target && e.target.closest ? e.target.closest('[data-copypath]') : null;
|
|
1929
|
+
if (t) {
|
|
1930
|
+
e.preventDefault(); e.stopPropagation();
|
|
1931
|
+
var rel = t.getAttribute('data-rename');
|
|
1932
|
+
var name = rel.split(/[\\/]/).pop() || rel;
|
|
1933
|
+
var nn = window.prompt('重命名文件(仅改名,不跨目录)', name);
|
|
1934
|
+
if (nn == null || String(nn).trim() === '' || String(nn).trim() === name) return;
|
|
1935
|
+
fetch('/api/dsh-uploads/workspace-file/rename', {
|
|
1936
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1937
|
+
body: JSON.stringify({ path: rel, newName: String(nn).trim() })
|
|
1938
|
+
}).then(function (r) { return r.json(); }).then(function (d) {
|
|
1939
|
+
if (d && d.ok) { location.reload(); }
|
|
1940
|
+
else { showMsg('重命名失败:' + ((d && d.error) || '未知'), 'error'); }
|
|
1941
|
+
}).catch(function (err) { showMsg('重命名失败:' + err, 'error'); });
|
|
1942
|
+
} else if (c) {
|
|
1943
|
+
e.preventDefault(); e.stopPropagation();
|
|
1944
|
+
var rel2 = c.getAttribute('data-copypath');
|
|
1945
|
+
try {
|
|
1946
|
+
navigator.clipboard.writeText(rel2).then(function () { showMsg('已复制路径:' + rel2); }, function () { showMsg('复制失败', 'error'); });
|
|
1947
|
+
} catch (e2) { showMsg('复制失败', 'error'); }
|
|
1948
|
+
}
|
|
1949
|
+
});
|
|
1950
|
+
|
|
1591
1951
|
// 工作区概览(仪表盘):从各文件行的 mtime/size 统计 今日/7日/全部/总大小。
|
|
1592
1952
|
(function () {
|
|
1593
1953
|
var rows = Array.prototype.slice.call(document.querySelectorAll('tr[data-ts]'));
|
|
@@ -2081,6 +2441,18 @@ export async function apply(ctx, config = {}) {
|
|
|
2081
2441
|
handler: handlers.pptxPreviewPage,
|
|
2082
2442
|
}), "dsh-long-plugins: pptx real-preview route");
|
|
2083
2443
|
|
|
2444
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2445
|
+
kind: "exact",
|
|
2446
|
+
path: "/api/dsh-uploads/xlsx-preview",
|
|
2447
|
+
handler: handlers.xlsxPreviewPage,
|
|
2448
|
+
}), "dsh-long-plugins: xlsx real-preview route");
|
|
2449
|
+
|
|
2450
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2451
|
+
kind: "exact",
|
|
2452
|
+
path: "/api/dsh-uploads/xlsx-preview-asset",
|
|
2453
|
+
handler: handlers.xlsxPreviewAsset,
|
|
2454
|
+
}), "dsh-long-plugins: xlsx-preview vendor assets");
|
|
2455
|
+
|
|
2084
2456
|
ctx.effect(() => ctx.webServer.register({
|
|
2085
2457
|
kind: "exact",
|
|
2086
2458
|
path: "/api/dsh-uploads/workspace-browse",
|
|
@@ -2121,6 +2493,12 @@ export async function apply(ctx, config = {}) {
|
|
|
2121
2493
|
handler: handlers.workspaceSave,
|
|
2122
2494
|
}), "dsh-long-plugins: workspace save route");
|
|
2123
2495
|
|
|
2496
|
+
ctx.effect(() => ctx.webServer.register({
|
|
2497
|
+
kind: "exact",
|
|
2498
|
+
path: "/api/dsh-uploads/workspace-file/rename",
|
|
2499
|
+
handler: handlers.workspaceRename,
|
|
2500
|
+
}), "dsh-long-plugins: workspace rename route");
|
|
2501
|
+
|
|
2124
2502
|
// ---- 技能文档 (skill docs) routes ----
|
|
2125
2503
|
ctx.effect(() => ctx.webServer.register({
|
|
2126
2504
|
kind: "exact",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-long-plugins",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
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",
|