dsh-long-plugins 2.4.6 → 2.6.0

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/lib/index.js CHANGED
@@ -718,67 +718,6 @@ async function officePreviewHtml(name, buffer) {
718
718
  }
719
719
 
720
720
  /** 仅保留毛玻璃配置允许字段并 clamp,避免写入任意/超大内容。背景图只收 data:image/*;base64 且限 2MB。 */
721
- function sanitizeGlass(input) {
722
- const out = {};
723
- if ("enabled" in input) out.enabled = input.enabled === true;
724
- if ("blur" in input) { const n = Number(input.blur); out.blur = Number.isFinite(n) ? Math.max(0, Math.min(80, Math.round(n))) : undefined; }
725
- if ("mask" in input) { const n = Number(input.mask); out.mask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
726
- if ("color" in input) out.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(input.color)) ? String(input.color) : undefined;
727
- const sanitizeUri = (v) => { const s = String(v ?? ""); return (/^data:image\/(png|jpe?g|webp|gif);base64,/.test(s) && s.length <= 2 * 1024 * 1024 + 256) ? s : undefined; };
728
- if ("bgImage" in input) out.bgImage = sanitizeUri(input.bgImage);
729
- if ("bgColor" in input) out.bgColor = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(input.bgColor)) ? String(input.bgColor) : undefined;
730
- if ("bgMask" in input) { const n = Number(input.bgMask); out.bgMask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
731
- if ("bgBlur" in input) { const n = Number(input.bgBlur); out.bgBlur = Number.isFinite(n) ? Math.max(0, Math.min(80, n)) : undefined; }
732
- if ("bgOpacity" in input) { const n = Number(input.bgOpacity); out.bgOpacity = Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : undefined; }
733
- // 背景罩(整页背景图上叠的罩色):浅/深主题各一套 color + mask(罩强度)
734
- if (input.bgTint && typeof input.bgTint === "object") {
735
- const bt = {};
736
- for (const key of ["light", "dark"]) {
737
- const z = input.bgTint[key];
738
- if (z && typeof z === "object") {
739
- const o = {};
740
- if ("color" in z) o.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(z.color)) ? String(z.color) : "#1a2332";
741
- if ("mask" in z) { const n = Number(z.mask); o.mask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : 0.28; }
742
- bt[key] = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
743
- }
744
- }
745
- if (Object.keys(bt).length) out.bgTint = bt;
746
- }
747
- // 输入框(独立于 zone.input 的 DSH 玻璃变量):浅/深主题各一套 custom color + opacity(1=实底)。color 空串=用主题原生色
748
- if (input.inputBox && typeof input.inputBox === "object") {
749
- const ib = {};
750
- for (const key of ["light", "dark"]) {
751
- const z = input.inputBox[key];
752
- if (z && typeof z === "object") {
753
- const o = {};
754
- const c = z.color;
755
- if (c === "" || c === null || c === undefined) o.color = "";
756
- else o.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(c)) ? String(c) : "";
757
- if ("opacity" in z) { const n = Number(z.opacity); o.opacity = Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1; }
758
- ib[key] = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
759
- }
760
- }
761
- if (Object.keys(ib).length) out.inputBox = ib;
762
- }
763
- // 会话区/输入区各自独立罩色+罩强度
764
- if (input.zone && typeof input.zone === "object") {
765
- const zo = {};
766
- for (const key of ["session", "input"]) {
767
- const z = input.zone[key];
768
- if (z && typeof z === "object") {
769
- const o = {};
770
- if ("enabled" in z) o.enabled = z.enabled === true;
771
- if ("color" in z) o.color = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(String(z.color)) ? String(z.color) : undefined;
772
- if ("mask" in z) { const n = Number(z.mask); o.mask = Number.isFinite(n) ? Math.max(0, Math.min(0.95, n)) : undefined; }
773
- if ("opacity" in z) { const n = Number(z.opacity); o.opacity = Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : undefined; }
774
- zo[key] = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
775
- }
776
- }
777
- if (Object.keys(zo).length) out.zone = zo;
778
- }
779
- return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== undefined));
780
- }
781
-
782
721
  export function createHandlers(options = {}) {
783
722
  const root = resolve(options.root || resolveUploadRoot());
784
723
  const maxFileBytes = positiveInteger(options.maxFileBytes, resolveMaxFileBytes());
@@ -1134,6 +1073,10 @@ export function createHandlers(options = {}) {
1134
1073
  // Markdown → 直接渲染成 HTML(真实效果),失败回退源码文本。
1135
1074
  editable = true; rawText = buffer.toString("utf8");
1136
1075
  body = `<article class="md">${markdownToHtml(rawText)}</article>`;
1076
+ } else if (/\.(html?|xhtml)$/i.test(name)) {
1077
+ // HTML/HTM → 直接渲染成实际效果(iframe + srcdoc, 保留其样式/脚本)
1078
+ editable = true; rawText = buffer.toString("utf8");
1079
+ body = `<iframe class="html" srcDoc="${escapeHtml(rawText)}"></iframe>`;
1137
1080
  } 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)) {
1138
1081
  editable = true; rawText = buffer.toString("utf8");
1139
1082
  body = `<pre class="text">${escapeHtml(rawText)}</pre>`;
@@ -1672,160 +1615,6 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1672
1615
  }
1673
1616
  };
1674
1617
 
1675
- // ---- 毛玻璃界面 (glass UI) 配置:读写 ~/.dsh-long-plugins/glass.json ----
1676
- // 背景图以 data-URI 存进配置,随配置持久化。enabled=总开关;blur=磨砂模糊;mask=整页罩强度;
1677
- // color=叠加/罩色;bgImage=共用背景图。顶栏/左栏的"罩"与整页共用 mask+color(不再单独存,简化)。
1678
- const GLASS_DEFAULTS = {
1679
- enabled: false, blur: 20, bgImage: "", bgColor: "#1a2332", bgMask: 0.28,
1680
- bgTint: { light: { color: "#1a2332", mask: 0.28 }, dark: { color: "#1a2332", mask: 0.28 } },
1681
- inputBox: { light: { color: "", opacity: 1 }, dark: { color: "", opacity: 1 } },
1682
- zone: { session: { color: "#1a2332", mask: 0.45, opacity: 0.5 }, input: { color: "#1a2332", mask: 0.6, opacity: 0.55 } },
1683
- // 各模块开关(「dsh-long」设置区):false=禁用该模块功能(避免与 DSH 新版自带功能冲突/按需关闭)
1684
- modules: { glass: false, uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: false },
1685
- };
1686
- const glassDir = resolve(homedir(), ".dsh-long-plugins");
1687
- const glassFile = join(glassDir, "glass.json");
1688
- const bgDir = join(glassDir, "backgrounds");
1689
- const currentBgFile = join(bgDir, "current.uri");
1690
- // 背景图文件引用: 配置里 bgImage 为 "current"(存 bgDir/current.uri) 或 ""(无) ;不再内嵌大图 data-URI
1691
- const decodeDataUri = (s) => { const m = /^data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=]+)$/.exec(String(s || "")); return m ? { mime: m[1], base64: m[2] } : null; };
1692
- const bgImageUrl = (cfg) => (cfg.bgImage === "current") ? "/api/dsh-uploads/glass-background" : "";
1693
- // 只保留当前背景图(current.uri), 删除其它历史图片文件(旧图清理)
1694
- async function pruneBackgrounds() {
1695
- try {
1696
- const names = await readdir(bgDir);
1697
- for (const n of names) {
1698
- if (n === "current.uri" || !n.endsWith(".uri")) continue;
1699
- try { await unlink(join(bgDir, n)); } catch (_) {}
1700
- }
1701
- } catch (_) {}
1702
- }
1703
- async function readGlassJSON() {
1704
- try {
1705
- const raw = JSON.parse(await readFile(glassFile, "utf8"));
1706
- const cfg = { ...GLASS_DEFAULTS, ...(raw && typeof raw === "object" ? raw : {}) };
1707
- // 迁移旧版: bgImage 是内嵌 data-URI(大图) → 存成文件 + 改引用
1708
- if (typeof cfg.bgImage === "string" && cfg.bgImage.startsWith("data:image/")) {
1709
- try {
1710
- await mkdir(bgDir, { recursive: true, mode: 0o700 });
1711
- await writeFile(currentBgFile, cfg.bgImage, "utf8");
1712
- cfg.bgImage = "current";
1713
- await writeFile(glassFile, JSON.stringify(cfg, null, 2), "utf8");
1714
- } catch (_) {}
1715
- }
1716
- // 插件升级时:版本号变化 → 默认关闭「会话导航」「RA-Span」(避免与 DSH 新版自带功能冲突)
1717
- if (PLUGIN_VERSION && cfg._pluginVersion !== PLUGIN_VERSION) {
1718
- try {
1719
- cfg.modules = { glass: false, uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: false, ...(cfg.modules || {}) };
1720
- cfg.modules.glass = false;
1721
- cfg.modules.turnRuler = false;
1722
- cfg._pluginVersion = PLUGIN_VERSION;
1723
- await writeFile(glassFile, JSON.stringify(cfg, null, 2), "utf8");
1724
- } catch (_) {}
1725
- }
1726
- return cfg;
1727
- } catch { return { ...GLASS_DEFAULTS }; }
1728
- }
1729
- const glassConfig = async (req, res) => {
1730
- try {
1731
- requireTrusted(req);
1732
- if (req.method === "GET" || req.method === "HEAD") {
1733
- const cfg = await readGlassJSON();
1734
- // 背景图只返回"是否存在 + 图片 URL", 不再返回内嵌大图 data-URI(减小响应)
1735
- let imgMeta = { present: false, bytes: 0 };
1736
- let imgUrl = "";
1737
- if (cfg.bgImage === "current") {
1738
- imgUrl = "/api/dsh-uploads/glass-background";
1739
- try { imgMeta = { present: true, bytes: (await stat(currentBgFile)).size || 0 }; } catch (_) { imgMeta = { present: true, bytes: 0 }; }
1740
- }
1741
- sendJson(res, 200, {
1742
- found: true,
1743
- cfg: {
1744
- enabled: cfg.enabled, blur: cfg.blur,
1745
- session: cfg.zone?.session || { enabled: true, color: "#1a2332", mask: 0.45, opacity: 0.5 },
1746
- input: cfg.zone?.input || { color: "#1a2332", mask: 0.6, opacity: 0.55 },
1747
- inputBox: cfg.inputBox || { light: { color: "", opacity: 1 }, dark: { color: "", opacity: 1 } },
1748
- bgTint: cfg.bgTint || { light: { color: "#1a2332", mask: 0.28 }, dark: { color: "#1a2332", mask: 0.28 } },
1749
- bgColor: cfg.bgColor || "#1a2332", bgMask: cfg.bgMask ?? 0.28,
1750
- bgBlur: cfg.bgBlur ?? 0, bgOpacity: cfg.bgOpacity ?? 1,
1751
- turnRuler: (cfg.modules && cfg.modules.turnRuler) !== false,
1752
- modules: { glass: false, uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: false, ...(cfg.modules || {}) },
1753
- bgImage: imgMeta,
1754
- },
1755
- bgImage: imgMeta,
1756
- bgImageUrl: imgUrl,
1757
- });
1758
- return;
1759
- }
1760
- if (req.method === "POST") {
1761
- const body = await readJsonBody(req, 4 * 1024 * 1024);
1762
- const current = await readGlassJSON();
1763
- const next = { ...current, ...sanitizeGlass(typeof body === "object" && body !== null ? body : {}) };
1764
- // 各模块开关表:body.modules 里有的键按布尔合并
1765
- if (body && typeof body.modules === "object" && body.modules !== null) {
1766
- const m = { glass: true, uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: true, ...(current.modules || {}) };
1767
- for (const k of Object.keys(body.modules)) m[k] = body.modules[k] !== false;
1768
- next.modules = m;
1769
- }
1770
- // 背景图改为"文件 + 引用": 用户上传的 data-URI 存入 current.uri, 配置只存 "current" 引用
1771
- if (typeof next.bgImage === "string" && next.bgImage.startsWith("data:image/")) {
1772
- try {
1773
- await mkdir(bgDir, { recursive: true, mode: 0o700 });
1774
- await writeFile(currentBgFile, next.bgImage, "utf8");
1775
- next.bgImage = "current";
1776
- } catch (_) { next.bgImage = ""; }
1777
- } else if ("bgImage" in body && body.bgImage === "") {
1778
- next.bgImage = ""; // 显式清除
1779
- try { await unlink(currentBgFile); } catch (_) {}
1780
- }
1781
- // 只保留当前背景图, 删除历史旧图
1782
- await pruneBackgrounds();
1783
- await mkdir(glassDir, { recursive: true, mode: 0o700 });
1784
- await writeFile(glassFile, JSON.stringify(next, null, 2), "utf8");
1785
- sendJson(res, 200, { ok: true, saved: true });
1786
- return;
1787
- }
1788
- methodNotAllowed(res, ["GET", "HEAD", "POST"]);
1789
- } catch (error) {
1790
- sendError(res, error, onError);
1791
- }
1792
- };
1793
- // 背景图历史(供设置面板"选择旧图"):GET 列出,GET ?name= 返回对应 data-URI
1794
- const glassBackgrounds = async (req, res) => {
1795
- try {
1796
- requireTrusted(req);
1797
- const url = new URL(req.url, "http://x");
1798
- const name = url.searchParams.get("name");
1799
- if (name) {
1800
- if (!/^[\w.:-]+\.uri$/.test(name)) { sendError(res, new Error("bad name"), onError); return; }
1801
- try { const buf = await readFile(join(bgDir, name)); res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); res.end(buf); return; }
1802
- catch { sendJson(res, 404, { error: "not found" }); return; }
1803
- }
1804
- let list = [];
1805
- // 只保留当前背景图, 删除历史旧图; 列表排除当前图
1806
- try { await pruneBackgrounds(); } catch (_) {}
1807
- try { list = (await readdir(bgDir)).filter((n) => n.endsWith(".uri") && n !== "current.uri").sort().reverse().slice(0, 30); } catch (_) {}
1808
- sendJson(res, 200, { list });
1809
- } catch (error) {
1810
- sendError(res, error, onError);
1811
- }
1812
- };
1813
- // 返回当前背景图(文件存的 data-URI 解码为真实图片)
1814
- const glassBackground = async (req, res) => {
1815
- try {
1816
- requireTrusted(req);
1817
- const cfg = await readGlassJSON();
1818
- if (cfg.bgImage !== "current") { sendJson(res, 404, { error: "no background" }); return; }
1819
- const buf = await readFile(currentBgFile, "utf8");
1820
- const d = decodeDataUri(buf);
1821
- if (!d) { sendJson(res, 404, { error: "bad image" }); return; }
1822
- const bytes = Buffer.from(d.base64, "base64");
1823
- res.writeHead(200, { "content-type": d.mime, "cache-control": "no-store", "x-content-type-options": "nosniff" });
1824
- res.end(bytes);
1825
- } catch (error) {
1826
- sendError(res, error, onError);
1827
- }
1828
- };
1829
1618
  // 补丁状态:只读检测 DSH 核心各补丁的「已打 / 未打 / 原生无需」,供 dsh-long 设置区显示
1830
1619
  const patchStatus = async (req, res) => {
1831
1620
  try {
@@ -1859,7 +1648,42 @@ window.addEventListener('resize',function(){ try{ if(fitMode) zoomFit(); else ap
1859
1648
  }
1860
1649
  };
1861
1650
 
1862
- return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, glassConfig, glassBackgrounds, glassBackground, patchStatus };
1651
+ // 各模块开关(dsh-long 设置区):读写 ~/.dsh-long-plugins/modules.json(独立于 dsh-span RA-Span 配置)。
1652
+ const MODULES_DEFAULTS = { uploadAttach: true, uploadDragDrop: true, uploadPaste: true, uploadPreview: true, skillDocs: true, balance: true, mobile: true, workspace: true, turnRuler: false };
1653
+ const modulesFile = join(homedir(), ".dsh-long-plugins", "modules.json");
1654
+ async function readModulesJSON() {
1655
+ try {
1656
+ const raw = JSON.parse(await readFile(modulesFile, "utf8"));
1657
+ return { ...MODULES_DEFAULTS, ...(raw && typeof raw === "object" && raw.modules ? raw.modules : {}) };
1658
+ } catch { return { ...MODULES_DEFAULTS }; }
1659
+ }
1660
+ const modulesConfig = async (req, res) => {
1661
+ try {
1662
+ requireTrusted(req);
1663
+ if (req.method === "GET" || req.method === "HEAD") {
1664
+ const modules = await readModulesJSON();
1665
+ sendJson(res, 200, { ok: true, cfg: { modules } });
1666
+ return;
1667
+ }
1668
+ if (req.method === "POST") {
1669
+ const body = await readJsonBody(req, 1024 * 1024);
1670
+ const current = await readModulesJSON();
1671
+ const next = { ...current };
1672
+ if (body && typeof body.modules === "object" && body.modules !== null) {
1673
+ for (const k of Object.keys(body.modules)) next[k] = body.modules[k] !== false;
1674
+ }
1675
+ await mkdir(join(homedir(), ".dsh-long-plugins"), { recursive: true, mode: 0o700 });
1676
+ await writeFile(modulesFile, JSON.stringify({ modules: next }, null, 2), "utf8");
1677
+ sendJson(res, 200, { ok: true, saved: true });
1678
+ return;
1679
+ }
1680
+ methodNotAllowed(res, ["GET", "HEAD", "POST"]);
1681
+ } catch (error) {
1682
+ sendError(res, error, onError);
1683
+ }
1684
+ };
1685
+
1686
+ return { root, maxFileBytes, totalMaxBytes, api, download, preview, workspaceList, workspaceFile, workspacePreview, workspaceBrowse, workspaceDelete, workspaceRename, workspaceSave, docxPreviewPage, docxPreviewAsset, pptxPreviewPage, xlsxPreviewPage, xlsxPreviewAsset, patchStatus, modulesConfig };
1863
1687
  }
1864
1688
 
1865
1689
  /** 人类可读文件大小。 */
@@ -2553,6 +2377,7 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "", e
2553
2377
  body.maximized .md { max-width:none; }
2554
2378
  .image { max-width:100%; border-radius:8px; }
2555
2379
  .pdf { display:block; width:100%; height:86vh; border:1px solid var(--lp-border); border-radius:8px; background:#fff; }
2380
+ .html { display:block; width:100%; height:86vh; border:1px solid var(--lp-border); border-radius:8px; background:#fff; }
2556
2381
  .office { background:#fff; color:#111; border-radius:8px; padding:16px; overflow:auto; }
2557
2382
  .unsupported { color:#f59e0b; text-align:center; padding:40px 0; }
2558
2383
  /* 放大模式:顶栏常驻(固定悬浮),内容占满视口、PDF 全高 */
@@ -2560,6 +2385,7 @@ function previewPageHtml(name, rel, size, downloadHref, body, inlineHref = "", e
2560
2385
  body.maximized .content { max-width:none; padding:0; margin:0; height:100vh; padding-top:53px; }
2561
2386
  body.maximized .md { padding:24px 32px 48px; max-width:820px; margin:0 auto; }
2562
2387
  body.maximized .pdf { height:calc(100vh - 53px); border:none; border-radius:0; }
2388
+ body.maximized .html { height:calc(100vh - 53px); border:none; border-radius:0; }
2563
2389
  body.maximized .text { height:calc(100vh - 53px); border:none; border-radius:0; overflow:auto; }
2564
2390
  body.maximized .office { height:calc(100vh - 53px); overflow:auto; }
2565
2391
  body.maximized .image { max-width:100vw; max-height:100vh; object-fit:contain; }
@@ -2644,72 +2470,6 @@ export async function apply(ctx, config = {}) {
2644
2470
 
2645
2471
  const handlers = createHandlers({ trustedHosts, onError, excludedWorkspaceNames: config.excludedWorkspaceNames });
2646
2472
 
2647
- // ---- md2docx 工具: Markdown → Word (.docx, 带页码) ----
2648
- // 调用 md2docx.py (pandoc-free, python-docx + 页脚 PAGE 字段)。
2649
- // 默认脚本随插件包分发(lib/md2docx.py),换机器也可靠;可用 config.md2docxScript 覆盖。
2650
- const PACKAGE_DIR = dirname(fileURLToPath(import.meta.url));
2651
- const MD2DOCX_SCRIPT = resolve(config.md2docxScript ?? join(PACKAGE_DIR, "md2docx.py"));
2652
- const runScript = (script, args) => new Promise((resolvePromise) => {
2653
- const child = spawn("python3", [script, ...args], { stdio: ["ignore", "pipe", "pipe"] });
2654
- let stdout = "", stderr = "";
2655
- child.stdout.on("data", (d) => { stdout += d.toString(); });
2656
- child.stderr.on("data", (d) => { stderr += d.toString(); });
2657
- child.on("error", (error) => resolvePromise({ ok: false, error: String(error), stdout, stderr }));
2658
- child.on("close", (code) => resolvePromise({ ok: code === 0, code, stdout, stderr }));
2659
- });
2660
- ctx.tools.register(defineTool({
2661
- name: "md2docx",
2662
- description: "Convert a Markdown file to a styled Word (.docx) document with a page-number footer. Ships a bundled python-docx script (lib/md2docx.py) that renders headings, tables, bold/italic, and lists; the docx includes a footer '第 N 页' field that updates when opened in Word or exported to PDF. Requires python3 and python-docx installed on the host. Override the script path via config.md2docxScript if needed.",
2663
- parameters: {
2664
- input: {
2665
- type: "string",
2666
- required: true,
2667
- description: "Absolute path to the input .md file."
2668
- },
2669
- output: {
2670
- type: "string",
2671
- description: "Optional absolute path for the output .docx. Defaults to the input path with a .docx extension."
2672
- }
2673
- },
2674
- output: {
2675
- schema: {
2676
- type: "object",
2677
- additionalProperties: false,
2678
- properties: {
2679
- ok: { type: "boolean", required: true },
2680
- docxPath: { type: "string" },
2681
- error: { type: "string" }
2682
- }
2683
- },
2684
- render: (_args, value) => [{
2685
- type: "text",
2686
- text: value && value.ok === true
2687
- ? `已生成 Word 文档:${value.docxPath}\n(含页码页脚,Word/另存 PDF 时自动更新)`
2688
- : `md2docx 失败:${value?.error ?? "未知错误"}`
2689
- }]
2690
- },
2691
- // 声明交付物:DSH 从 presentCall 的 locations 识别本工具产出的文件,
2692
- // 从而把 docx 渲染成可点击的交付物卡片(消息里的文件引用也能打开)。
2693
- presentCall: (args) => {
2694
- const inPath = resolve(String(args.input ?? ""));
2695
- const outPath = args.output ? resolve(String(args.output)) : inPath.replace(/\.md$/i, ".docx");
2696
- return {
2697
- card: "generic",
2698
- title: "md2docx",
2699
- kind: "edit",
2700
- locations: [{ path: outPath }]
2701
- };
2702
- },
2703
- async execute(args) {
2704
- const inPath = resolve(String(args.input ?? ""));
2705
- const outPath = args.output ? resolve(String(args.output)) : inPath.replace(/\.md$/i, ".docx");
2706
- const result = await runScript(MD2DOCX_SCRIPT, [inPath, outPath]);
2707
- if (!result.ok) {
2708
- return { ok: false, error: (result.stderr || result.stdout || String(result.error)).trim() || `md2docx failed (exit ${result.code})` };
2709
- }
2710
- return { ok: true, docxPath: outPath };
2711
- }
2712
- }));
2713
2473
 
2714
2474
  await sweepUploadTemps(handlers.root);
2715
2475
 
@@ -2784,29 +2544,16 @@ export async function apply(ctx, config = {}) {
2784
2544
  }), "dsh-long-plugins: xlsx-preview vendor assets");
2785
2545
 
2786
2546
  ctx.effect(() => ctx.webServer.register({
2787
- kind: "exact",
2788
- path: "/api/dsh-uploads/glass-config",
2789
- handler: handlers.glassConfig,
2790
- }), "dsh-long-plugins: glass-ui config route");
2791
-
2792
- ctx.effect(() => ctx.webServer.register({
2793
- kind: "exact",
2794
- path: "/api/dsh-uploads/glass-backgrounds",
2795
- handler: handlers.glassBackgrounds,
2796
- }), "dsh-long-plugins: glass-ui backgrounds route");
2797
-
2798
- ctx.effect(() => ctx.webServer.register({
2799
- kind: "exact",
2800
- path: "/api/dsh-uploads/glass-background",
2801
- handler: handlers.glassBackground,
2802
- }), "dsh-long-plugins: glass-ui background image route");
2803
-
2804
- ctx.effect(() => ctx.webServer.register({
2805
- kind: "exact",
2806
2547
  path: "/api/dsh-uploads/patch-status",
2807
2548
  handler: handlers.patchStatus,
2808
2549
  }), "dsh-long-plugins: patch status route");
2809
2550
 
2551
+ ctx.effect(() => ctx.webServer.register({
2552
+ kind: "exact",
2553
+ path: "/api/dsh-uploads/modules-config",
2554
+ handler: handlers.modulesConfig,
2555
+ }), "dsh-long-plugins: modules config route");
2556
+
2810
2557
  ctx.effect(() => ctx.webServer.register({
2811
2558
  kind: "exact",
2812
2559
  path: "/api/dsh-uploads/workspace-browse",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-long-plugins",
3
- "version": "2.4.6",
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 心跳、升级重连)。",
3
+ "version": "2.6.0",
4
+ "description": "Merged DSH web plugins: upload manager (drag-and-drop attach any file to the message), workspace output files, skill docs, account balance, 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 的常用增强:上传管理(拖放上传:拖任意文件到会话框直接附加)、工作区「输出文件」面板、技能文档浏览、账户余额显示、Word/PowerPoint 真实预览(可缩放、翻页);并自带修复安装,自动重连 DSH 核心补丁(反代 WebSocket 心跳、升级重连)。Office 读取/生成已独立到 dsh-office-reader;RA-Span 已独立到 dsh-span。",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "main": "lib/index.js",
@@ -12,7 +12,6 @@
12
12
  },
13
13
  "files": [
14
14
  "lib",
15
- "lib/md2docx.py",
16
15
  "client",
17
16
  "!client/*.bak",
18
17
  "cordis.patch.yml",
@@ -91,6 +91,29 @@ fi
91
91
  9. 设置面板「技能管理」「上传文件」「输出文件」的 🔍 搜索框:点击后弹出框紧贴按钮下方、滚动不脱离。
92
92
  10. **「技能管理」区(全局技能 / 工作区技能两 tab)**:全局技能列 `<DSH_HOME>/skills`;工作区技能扫描工作区根下各子目录的 `.dsh/skills`(无 `.dsh/skills` 则该工作区组为空)。若该区不出现/工作区技能为空,可能 DSH `settings.section` 槽或工作区结构变化——复核。
93
93
 
94
+ ## 插件依赖 DSH 内部接口(升级脆弱清单)⚠️
95
+ dsh-long-plugins 大量依赖 DSH 内部接口/结构,DSH 升级改动后这些会**静默失效**(不注册/不显示/报错)。升级后逐项复核:
96
+
97
+ | 依赖 | 用途 | 升级失效表现 |
98
+ |---|---|---|
99
+ | `settings.section` 槽 | 上传文件/输出文件/技能管理/dsh-long/RA-Span 设置区 | 对应设置区不出现 |
100
+ | `conversation.composer.dock` 槽 | 余额 chip | 余额不显示 |
101
+ | `conversation.input.dock` 槽 | 待发送文件栏、拖放/粘贴上传 | 待发送栏/拖放失效 |
102
+ | `conversation.input.left` 槽 | 附件上传(回形针) | 回形针消失 |
103
+ | `conversation.session.header.actions` 槽 | 会话历史/工作区按钮 | 按钮消失 |
104
+ | `shell.overlay` 槽 | 文件浏览器浮层 | 文件浮层打不开 |
105
+ | `data-slot-conversation="..."` 属性 | 余额 chip / dock 样式定位 | 对应样式失效 |
106
+ | `ctx.webServer.register` | 所有路由(上传/工作区/技能/预览/glass/补丁状态) | 路由 404/无响应 |
107
+ | `ctx.inputTriggers.registerSource` | 文件引用 codec(`@`) | 附件引用失效 |
108
+ | `ctx.locale.register` | 多语言 | 文案不翻译 |
109
+ | `window.__ModuleLoader__.load` | 插件客户端加载 | 插件整个不加载 |
110
+ | `useSessions` / 会话 store | 工作区按钮拿 cwd / 当前工作区 | 工作区按钮/当前工作区失效 |
111
+ | 混淆类名 `wSkVaW_/uV2eYG_/o3BgMG_/CY-8Ka_/QWLzLg_/M8wy4a_/lXshSW_/7yHdaG_/VOzbGW_/p-xYUq_/osXY9a_` | RA-Span 玻璃观感/排版 | 观感/配色/排版错(见"RA-Span 升级后需重新校准") |
112
+ | DSH 设置面板 DOM(`VOzbGW` 面板) | 各设置区在其内渲染 | 设置区排版乱 |
113
+ | DSH 核心 `dsh-client-connection` | 心跳补丁(补丁3) | 补丁被覆盖→心跳失效(需重打) |
114
+
115
+ > 处置:某项失效 → 让用户右键抓新元素 class/槽名/接口,更新插件对应选择器/槽名/调用;或集成 DSH 新 API。**稳定项**:插件自己的路由表、`data-slot` 状态栏(部分)、注入 `<head>` 的 CSS(部分)。
116
+
94
117
  ## 硬性安全边界(必须遵守)
95
118
  - **不主动 push / 打 tag / 发 Release**;需要发布版本时停下用 ask_user_question 等确认。
96
119
  - 升级会重启 DSH 服务,可能中断当前会话——先向用户说明再执行。
package/lib/md2docx.py DELETED
@@ -1,210 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """Generic Markdown -> styled .docx converter with a page-number footer.
4
-
5
- Usage:
6
- python3 md2docx.py <input.md> [output.docx]
7
-
8
- If output.docx is omitted, it defaults to <input-name>.docx next to the input.
9
-
10
- Renders headings (h1-h3), bold/italic inline, tables (pipe syntax),
11
- ordered/unordered lists, blockquotes, and horizontal rules. Adds a centered
12
- footer with an auto-updating PAGE field (updates when opened in Word or
13
- exported to PDF).
14
-
15
- Requires: python3 + python-docx (`pip install python-docx`).
16
- """
17
- import re
18
- import sys
19
- import os
20
-
21
- from docx import Document
22
- from docx.shared import Pt, RGBColor
23
- from docx.enum.text import WD_ALIGN_PARAGRAPH
24
- from docx.enum.table import WD_TABLE_ALIGNMENT
25
- from docx.oxml.ns import qn
26
- from docx.oxml import OxmlElement
27
-
28
-
29
- def die(msg):
30
- print(f"md2docx: {msg}", file=sys.stderr)
31
- sys.exit(1)
32
-
33
-
34
- if len(sys.argv) < 2:
35
- die("usage: md2docx.py <input.md> [output.docx]")
36
-
37
- SRC = os.path.abspath(sys.argv[1])
38
- if not os.path.isfile(SRC):
39
- die(f"input file not found: {SRC}")
40
- OUT = (
41
- os.path.abspath(sys.argv[2])
42
- if len(sys.argv) > 2
43
- else os.path.splitext(SRC)[0] + ".docx"
44
- )
45
-
46
- with open(SRC, encoding="utf-8") as f:
47
- lines = f.read().splitlines()
48
-
49
-
50
- def add_page_number(paragraph):
51
- """Insert an auto-updating '第 N 页' page field into a footer paragraph."""
52
- run = paragraph.add_run("第 ")
53
- set_east_asia(run)
54
- fld = OxmlElement("w:fldChar")
55
- fld.set(qn("w:fldCharType"), "begin")
56
- instr = OxmlElement("w:instrText")
57
- instr.set(qn("xml:space"), "preserve")
58
- instr.text = " PAGE "
59
- sep = OxmlElement("w:fldChar")
60
- sep.set(qn("w:fldCharType"), "separate")
61
- t = OxmlElement("w:t")
62
- t.text = "1"
63
- end = OxmlElement("w:fldChar")
64
- end.set(qn("w:fldCharType"), "end")
65
- r = paragraph.add_run()
66
- set_east_asia(r)
67
- for el in (fld, instr, sep, t, end):
68
- r._r.append(el)
69
- run2 = paragraph.add_run(" 页")
70
- set_east_asia(run2)
71
-
72
-
73
- doc = Document()
74
- doc.add_heading(os.path.splitext(os.path.basename(SRC))[0], level=0)
75
-
76
-
77
- # --- base styles ---
78
- normal = doc.styles["Normal"]
79
- normal.font.name = "Calibri"
80
- normal.font.size = Pt(10.5)
81
- normal._element.rPr.rFonts.set(qn("w:eastAsia"), "微软雅黑")
82
-
83
-
84
- def set_east_asia(run):
85
- run.font.name = "Calibri"
86
- r = run._element
87
- rPr = r.get_or_add_rPr()
88
- rf = rPr.find(qn("w:rFonts"))
89
- if rf is None:
90
- rf = OxmlElement("w:rFonts")
91
- rPr.append(rf)
92
- rf.set(qn("w:eastAsia"), "微软雅黑")
93
-
94
-
95
- def add_runs_with_bold(par, text):
96
- """Add text to paragraph, honoring **bold** and *italic* markers."""
97
- for tok in re.split(r"(\*\*.*?\*\*|\*.*?\*)", text):
98
- if not tok:
99
- continue
100
- if tok.startswith("**") and tok.endswith("**") and len(tok) > 4:
101
- r = par.add_run(tok[2:-2])
102
- r.bold = True
103
- elif tok.startswith("*") and tok.endswith("*") and len(tok) > 2:
104
- r = par.add_run(tok[1:-1])
105
- r.italic = True
106
- else:
107
- r = par.add_run(tok)
108
- set_east_asia(r)
109
-
110
-
111
- def add_body_paragraph(text, style=None):
112
- p = doc.add_paragraph(style=style)
113
- add_runs_with_bold(p, text)
114
- return p
115
-
116
-
117
- def flush_table():
118
- global table_rows, in_table
119
- if not table_rows:
120
- in_table = False
121
- return
122
- ncols = max(len(r) for r in table_rows)
123
- tbl = doc.add_table(rows=len(table_rows), cols=ncols)
124
- tbl.style = "Light Grid Accent 1"
125
- tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
126
- for ri, row in enumerate(table_rows):
127
- for ci in range(ncols):
128
- cell = tbl.cell(ri, ci)
129
- cell.text = ""
130
- cp = cell.paragraphs[0]
131
- text = row[ci] if ci < len(row) else ""
132
- add_runs_with_bold(cp, text)
133
- if ri == 0:
134
- for run in cp.runs:
135
- run.bold = True
136
- tbl.autofit = True
137
- table_rows = []
138
- in_table = False
139
-
140
-
141
- table_rows = []
142
- in_table = False
143
-
144
- i = 0
145
- while i < len(lines):
146
- stripped = lines[i].strip()
147
- if not stripped:
148
- i += 1
149
- continue
150
- if re.fullmatch(r"-{3,}", stripped):
151
- flush_table()
152
- doc.add_paragraph()
153
- i += 1
154
- continue
155
- if in_table and "|" in stripped and re.fullmatch(r"\|?[\s:|-]+\|?", stripped):
156
- i += 1
157
- continue
158
- if stripped.startswith("|"):
159
- in_table = True
160
- table_rows.append([c.strip() for c in stripped.strip("|").split("|")])
161
- i += 1
162
- continue
163
- if in_table:
164
- flush_table()
165
- if stripped.startswith("### "):
166
- h = doc.add_heading(level=3)
167
- add_runs_with_bold(h, stripped[4:])
168
- i += 1
169
- continue
170
- if stripped.startswith("## "):
171
- h = doc.add_heading(level=2)
172
- add_runs_with_bold(h, stripped[3:])
173
- i += 1
174
- continue
175
- if stripped.startswith("# "):
176
- h = doc.add_heading(level=1)
177
- add_runs_with_bold(h, stripped[2:])
178
- i += 1
179
- continue
180
- if stripped.startswith("> "):
181
- p = doc.add_paragraph(style="Intense Quote")
182
- add_runs_with_bold(p, stripped[2:] + " ")
183
- i += 1
184
- continue
185
- m = re.match(r"^(\d+)\.\s+(.*)$", stripped)
186
- if m:
187
- p = doc.add_paragraph(style="List Number")
188
- add_runs_with_bold(p, m.group(2))
189
- i += 1
190
- continue
191
- if stripped.startswith("- "):
192
- p = doc.add_paragraph(style="List Bullet")
193
- add_runs_with_bold(p, stripped[2:])
194
- i += 1
195
- continue
196
- p = doc.add_paragraph()
197
- add_runs_with_bold(p, stripped)
198
- i += 1
199
-
200
- if in_table:
201
- flush_table()
202
-
203
- # --- footer with page number field ---
204
- footer = doc.sections[0].footer
205
- fp = footer.paragraphs[0]
206
- fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
207
- add_page_number(fp)
208
-
209
- doc.save(OUT)
210
- print("Saved:", OUT)