@lijian-ui/dsh-file-manager 0.2.3 → 0.2.4
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/LICENSE +38 -38
- package/README.i18n.yaml +6 -6
- package/README.md +66 -66
- package/README.zh.md +66 -66
- package/cordis.patch.yml +13 -13
- package/lib/client.js +27305 -270
- package/lib/client.js.map +1 -1
- package/lib/index.js +474 -42
- package/lib/tsconfig.client.tsbuildinfo +1 -1
- package/lib/tsconfig.host.tsbuildinfo +1 -1
- package/lib/types/client/file-source.d.ts +10 -0
- package/lib/types/client/file-source.d.ts.map +1 -1
- package/lib/types/client/fileType.d.ts +19 -4
- package/lib/types/client/fileType.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/mount.d.ts +1 -1
- package/lib/types/client/mount.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
- package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
- package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
- package/lib/types/client/preview/content.d.ts +3 -1
- package/lib/types/client/preview/content.d.ts.map +1 -1
- package/lib/types/client/preview/office.d.ts +23 -0
- package/lib/types/client/preview/office.d.ts.map +1 -0
- package/lib/types/client/reference.d.ts +17 -0
- package/lib/types/client/reference.d.ts.map +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/host/fs-service.d.ts.map +1 -1
- package/lib/types/host/office-preview.d.ts +13 -0
- package/lib/types/host/office-preview.d.ts.map +1 -0
- package/lib/types/host/routes.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +19 -4
- package/src/client/FileManagerSettingsCard.tsx +117 -117
- package/src/client/PluginSettingsCard.tsx +337 -337
- package/src/client/chat/file-ref.ts +144 -144
- package/src/client/chat/mermaid-chat.tsx +102 -102
- package/src/client/chat/placeholder-hint.tsx +46 -46
- package/src/client/components/ExplorerPanel.tsx +534 -534
- package/src/client/components/FileIcon.tsx +46 -46
- package/src/client/components/ScmPanel.tsx +480 -480
- package/src/client/components/a11y.ts +20 -20
- package/src/client/components/icons.tsx +274 -274
- package/src/client/components/overlay.tsx +225 -225
- package/src/client/drag/DragFileInlay.tsx +89 -89
- package/src/client/drag/file-drag.ts +67 -67
- package/src/client/drag.ts +154 -154
- package/src/client/file-source.ts +34 -0
- package/src/client/fileType.ts +32 -5
- package/src/client/hooks/useResizableSplit.ts +91 -91
- package/src/client/hooks/useStore.ts +15 -15
- package/src/client/index.ts +42 -3
- package/src/client/layout.ts +523 -523
- package/src/client/locales.ts +6 -0
- package/src/client/maximize.ts +34 -34
- package/src/client/mount.tsx +2 -1
- package/src/client/persist.ts +194 -194
- package/src/client/picker/FilePickerModal.tsx +526 -526
- package/src/client/picker/file-picker.ts +86 -86
- package/src/client/preview/PreviewPanel.tsx +7 -1
- package/src/client/preview/PreviewTabs.tsx +159 -159
- package/src/client/preview/PreviewToolbar.tsx +3 -2
- package/src/client/preview/content.tsx +7 -2
- package/src/client/preview/markdown.ts +351 -351
- package/src/client/preview/mermaid.ts +274 -274
- package/src/client/preview/office.tsx +321 -0
- package/src/client/reference.ts +49 -0
- package/src/client/settings-card.module.css +316 -316
- package/src/client/settings-form.ts +451 -451
- package/src/client/store.ts +17 -12
- package/src/client/styles/drag.module.css +30 -30
- package/src/client/styles/explorer.module.css +371 -371
- package/src/client/styles/picker.module.css +344 -344
- package/src/client/styles/preview.module.css +3 -1
- package/src/client/styles/scm.module.css +380 -380
- package/src/client/styles/tokens.module.css +376 -371
- package/src/core/types.ts +162 -162
- package/src/host/fs-service.ts +7 -1
- package/src/host/gate.ts +73 -73
- package/src/host/git-runner.ts +116 -116
- package/src/host/git-service.ts +395 -395
- package/src/host/loopback.ts +63 -63
- package/src/host/office-preview.ts +399 -0
- package/src/host/poll-guard.ts +109 -109
- package/src/host/routes.ts +72 -1
- package/src/index.ts +3 -1
- package/src/mount-once.ts +48 -48
- package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
- package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
- package/lib/types/client/floating.d.ts +0 -27
- package/lib/types/client/floating.d.ts.map +0 -1
- package/lib/types/client/mention.d.ts +0 -44
- package/lib/types/client/mention.d.ts.map +0 -1
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import { mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative } from "node:path";
|
|
4
|
-
import { createReadStream } from "node:fs";
|
|
4
|
+
import { createReadStream, existsSync, watch } from "node:fs";
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
//#region \0rolldown/runtime.js
|
|
@@ -224,7 +224,7 @@ function installSettingsSection(ctx, ns, schema, entry, hooks) {
|
|
|
224
224
|
});
|
|
225
225
|
}
|
|
226
226
|
//#endregion
|
|
227
|
-
//#region node_modules/cosmokit/lib/index.cjs
|
|
227
|
+
//#region ../../node_modules/cosmokit/lib/index.cjs
|
|
228
228
|
var require_lib$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
229
229
|
var __defProp = Object.defineProperty;
|
|
230
230
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -1458,7 +1458,13 @@ function mimeByExtension(rel) {
|
|
|
1458
1458
|
ico: "image/x-icon",
|
|
1459
1459
|
avif: "image/avif",
|
|
1460
1460
|
bmp: "image/bmp",
|
|
1461
|
-
pdf: "application/pdf"
|
|
1461
|
+
pdf: "application/pdf",
|
|
1462
|
+
doc: "application/msword",
|
|
1463
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1464
|
+
xls: "application/vnd.ms-excel",
|
|
1465
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1466
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
1467
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
1462
1468
|
}[rel.split(".").pop()?.toLowerCase() ?? ""];
|
|
1463
1469
|
}
|
|
1464
1470
|
/** Derive the mime type for a raw read from the extension, then the content. */
|
|
@@ -2488,11 +2494,11 @@ function isLoopbackRequest(request) {
|
|
|
2488
2494
|
}
|
|
2489
2495
|
//#endregion
|
|
2490
2496
|
//#region src/host/routes.ts
|
|
2491
|
-
const OK = (value) => ({
|
|
2497
|
+
const OK$1 = (value) => ({
|
|
2492
2498
|
ok: true,
|
|
2493
2499
|
value
|
|
2494
2500
|
});
|
|
2495
|
-
const FAIL = (error) => ({
|
|
2501
|
+
const FAIL$1 = (error) => ({
|
|
2496
2502
|
ok: false,
|
|
2497
2503
|
error
|
|
2498
2504
|
});
|
|
@@ -2565,6 +2571,14 @@ const GIT_POLL_MS = 3e4;
|
|
|
2565
2571
|
/** SSE keep-alive comment interval (proxies drop idle connections). */
|
|
2566
2572
|
const HEARTBEAT_MS = 15e3;
|
|
2567
2573
|
/**
|
|
2574
|
+
* Debounce window for coalescing fs.watch events into one `{kind:'fs'}`
|
|
2575
|
+
* push. LLM/ad-hoc file writes (e.g. an agent creating work products with a
|
|
2576
|
+
* subprocess) often land several events in quick succession; a single trailing
|
|
2577
|
+
* push per burst keeps the client's refresh amortized (client also coalesces
|
|
2578
|
+
* with its own FS_COALESCE_MS).
|
|
2579
|
+
*/
|
|
2580
|
+
const FS_WATCH_DEBOUNCE_MS = 200;
|
|
2581
|
+
/**
|
|
2568
2582
|
* Parse a `Range: bytes=start-end` header against the file size. RFC 7233
|
|
2569
2583
|
* lets a server ignore any Range it does not support, so unknown units,
|
|
2570
2584
|
* malformed headers and multi-range requests all return null (the caller
|
|
@@ -2675,7 +2689,7 @@ function strArray(payload, key) {
|
|
|
2675
2689
|
return value;
|
|
2676
2690
|
}
|
|
2677
2691
|
/** Write one JSON envelope response. */
|
|
2678
|
-
function json(res, envelope, status = 200) {
|
|
2692
|
+
function json$1(res, envelope, status = 200) {
|
|
2679
2693
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
2680
2694
|
res.end(JSON.stringify(envelope));
|
|
2681
2695
|
}
|
|
@@ -2743,6 +2757,49 @@ function registerPanelRoutes(ctx, fs, git) {
|
|
|
2743
2757
|
gitPoll.stop();
|
|
2744
2758
|
gitPoll = void 0;
|
|
2745
2759
|
};
|
|
2760
|
+
let fsWatcher;
|
|
2761
|
+
let fsWatchTimer;
|
|
2762
|
+
const pushFsChange = () => {
|
|
2763
|
+
if (fsWatchTimer !== void 0) return;
|
|
2764
|
+
fsWatchTimer = setTimeout(() => {
|
|
2765
|
+
fsWatchTimer = void 0;
|
|
2766
|
+
for (const subscriber of subscribers) push(subscriber, { kind: "fs" });
|
|
2767
|
+
}, FS_WATCH_DEBOUNCE_MS);
|
|
2768
|
+
};
|
|
2769
|
+
/** Start watching one canonical root; recursive watch falls back to non-recursive when unsupported. */
|
|
2770
|
+
const startFsWatch = () => {
|
|
2771
|
+
if (fsWatcher !== void 0) return;
|
|
2772
|
+
let watcher;
|
|
2773
|
+
try {
|
|
2774
|
+
watcher = watch(subscribersRoot(), { recursive: true });
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
ctx.logger.warn(`dsh-filemgr: recursive fs.watch unavailable (${String(error)}), falling back to shallow watch`);
|
|
2777
|
+
try {
|
|
2778
|
+
watcher = watch(subscribersRoot());
|
|
2779
|
+
} catch (fallbackError) {
|
|
2780
|
+
ctx.logger.warn(`dsh-filemgr: fs.watch failed, live directory changes will not auto-refresh (${String(fallbackError)})`);
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
fsWatcher = watcher;
|
|
2785
|
+
const onEvent = () => pushFsChange();
|
|
2786
|
+
watcher.on("change", (_eventType, _filename) => onEvent());
|
|
2787
|
+
watcher.on("error", (error) => {
|
|
2788
|
+
ctx.logger.warn(`dsh-filemgr: fs.watch error (${String(error)}), live refresh degraded`);
|
|
2789
|
+
});
|
|
2790
|
+
ctx.logger.debug(`dsh-filemgr: watching ${subscribersRoot()} for live directory changes`);
|
|
2791
|
+
};
|
|
2792
|
+
const stopFsWatch = () => {
|
|
2793
|
+
if (fsWatchTimer !== void 0) {
|
|
2794
|
+
clearTimeout(fsWatchTimer);
|
|
2795
|
+
fsWatchTimer = void 0;
|
|
2796
|
+
}
|
|
2797
|
+
if (fsWatcher === void 0) return;
|
|
2798
|
+
fsWatcher.close();
|
|
2799
|
+
fsWatcher = void 0;
|
|
2800
|
+
};
|
|
2801
|
+
/** The canonical root of the single connected subscriber; '' when none. */
|
|
2802
|
+
const subscribersRoot = () => subscribers.size > 0 ? [...subscribers][0].root : "";
|
|
2746
2803
|
/**
|
|
2747
2804
|
* GET /filemgr/raw: stream one workspace file (markdown image srcs,
|
|
2748
2805
|
* pdf preview). Gated like every other operation; FsService.readRaw only
|
|
@@ -2757,13 +2814,13 @@ function registerPanelRoutes(ctx, fs, git) {
|
|
|
2757
2814
|
const root = url.searchParams.get("root");
|
|
2758
2815
|
const path = url.searchParams.get("path");
|
|
2759
2816
|
if (root === null || root === "" || path === null || path === "") {
|
|
2760
|
-
json(res, FAIL(BAD_REQUEST), 400);
|
|
2817
|
+
json$1(res, FAIL$1(BAD_REQUEST), 400);
|
|
2761
2818
|
return;
|
|
2762
2819
|
}
|
|
2763
2820
|
const result = await fs.readRaw(root, path);
|
|
2764
2821
|
if (!("abs" in result)) {
|
|
2765
2822
|
const status = result.code === "path-outside-root" || result.code === "is-directory" ? 403 : 404;
|
|
2766
|
-
json(res, FAIL(result), status);
|
|
2823
|
+
json$1(res, FAIL$1(result), status);
|
|
2767
2824
|
return;
|
|
2768
2825
|
}
|
|
2769
2826
|
const etag = `W/"${result.size}-${Math.floor(result.mtime)}"`;
|
|
@@ -2875,172 +2932,172 @@ function registerPanelRoutes(ctx, fs, git) {
|
|
|
2875
2932
|
return;
|
|
2876
2933
|
}
|
|
2877
2934
|
if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
2878
|
-
json(res, FAIL(BAD_REQUEST), 415);
|
|
2935
|
+
json$1(res, FAIL$1(BAD_REQUEST), 415);
|
|
2879
2936
|
return;
|
|
2880
2937
|
}
|
|
2881
2938
|
const pathname = new URL(req.url ?? "/", "http://x").pathname;
|
|
2882
2939
|
const payload = await readJsonBody(req);
|
|
2883
2940
|
if (payload === null) {
|
|
2884
|
-
json(res, FAIL(BAD_REQUEST));
|
|
2941
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2885
2942
|
return;
|
|
2886
2943
|
}
|
|
2887
2944
|
const root = strField(payload, "root");
|
|
2888
2945
|
if (root === null) {
|
|
2889
|
-
json(res, FAIL(BAD_REQUEST));
|
|
2946
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2890
2947
|
return;
|
|
2891
2948
|
}
|
|
2892
2949
|
switch (pathname) {
|
|
2893
2950
|
case "/filemgr/list": {
|
|
2894
2951
|
const path = strField(payload, "path") ?? "";
|
|
2895
2952
|
const result = await fs.list(root, path);
|
|
2896
|
-
json(res, "entries" in result ? OK(result) : FAIL(result));
|
|
2953
|
+
json$1(res, "entries" in result ? OK$1(result) : FAIL$1(result));
|
|
2897
2954
|
return;
|
|
2898
2955
|
}
|
|
2899
2956
|
case "/filemgr/read": {
|
|
2900
2957
|
const path = strField(payload, "path");
|
|
2901
2958
|
if (path === null) {
|
|
2902
|
-
json(res, FAIL(BAD_REQUEST));
|
|
2959
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2903
2960
|
return;
|
|
2904
2961
|
}
|
|
2905
2962
|
const asImage = typeof payload === "object" && payload !== null ? payload.asImage === true : false;
|
|
2906
2963
|
const result = await fs.read(root, path, asImage);
|
|
2907
|
-
json(res, "content" in result ? OK(result) : FAIL(result));
|
|
2964
|
+
json$1(res, "content" in result ? OK$1(result) : FAIL$1(result));
|
|
2908
2965
|
return;
|
|
2909
2966
|
}
|
|
2910
2967
|
case "/filemgr/write": {
|
|
2911
2968
|
const path = strField(payload, "path");
|
|
2912
2969
|
const content = strOrEmpty(payload, "content");
|
|
2913
2970
|
if (path === null || content === null) {
|
|
2914
|
-
json(res, FAIL(BAD_REQUEST));
|
|
2971
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2915
2972
|
return;
|
|
2916
2973
|
}
|
|
2917
2974
|
const rawBase = typeof payload === "object" && payload !== null ? payload.baseMtime : void 0;
|
|
2918
2975
|
const baseMtime = typeof rawBase === "number" && Number.isFinite(rawBase) ? rawBase : void 0;
|
|
2919
2976
|
const result = await fs.write(root, path, content, baseMtime);
|
|
2920
|
-
json(res, "mtime" in result ? OK(result) : FAIL(result));
|
|
2977
|
+
json$1(res, "mtime" in result ? OK$1(result) : FAIL$1(result));
|
|
2921
2978
|
return;
|
|
2922
2979
|
}
|
|
2923
2980
|
case "/filemgr/search": {
|
|
2924
2981
|
const query = strField(payload, "query") ?? "";
|
|
2925
2982
|
const result = await fs.search(root, query);
|
|
2926
|
-
json(res, "hits" in result ? OK(result) : FAIL(result));
|
|
2983
|
+
json$1(res, "hits" in result ? OK$1(result) : FAIL$1(result));
|
|
2927
2984
|
return;
|
|
2928
2985
|
}
|
|
2929
2986
|
case "/filemgr/delete": {
|
|
2930
2987
|
const path = strField(payload, "path");
|
|
2931
2988
|
if (path === null) {
|
|
2932
|
-
json(res, FAIL(BAD_REQUEST));
|
|
2989
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2933
2990
|
return;
|
|
2934
2991
|
}
|
|
2935
2992
|
const result = await fs.delete(root, path);
|
|
2936
|
-
json(res, "ok" in result ? OK(result) : FAIL(result));
|
|
2993
|
+
json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
|
|
2937
2994
|
return;
|
|
2938
2995
|
}
|
|
2939
2996
|
case "/filemgr/reveal": {
|
|
2940
2997
|
const path = strField(payload, "path");
|
|
2941
2998
|
if (path === null) {
|
|
2942
|
-
json(res, FAIL(BAD_REQUEST));
|
|
2999
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2943
3000
|
return;
|
|
2944
3001
|
}
|
|
2945
3002
|
const resolved = await fs.resolveAbsolute(root, path);
|
|
2946
3003
|
if (!("ok" in resolved)) {
|
|
2947
|
-
json(res, FAIL(resolved));
|
|
3004
|
+
json$1(res, FAIL$1(resolved));
|
|
2948
3005
|
return;
|
|
2949
3006
|
}
|
|
2950
3007
|
const error = spawnOsCommand(ctx, revealArgv(process.platform, resolved.abs));
|
|
2951
|
-
json(res, error === null ? OK({ ok: true }) : FAIL(error));
|
|
3008
|
+
json$1(res, error === null ? OK$1({ ok: true }) : FAIL$1(error));
|
|
2952
3009
|
return;
|
|
2953
3010
|
}
|
|
2954
3011
|
case "/filemgr/open-with-default": {
|
|
2955
3012
|
const path = strField(payload, "path");
|
|
2956
3013
|
if (path === null) {
|
|
2957
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3014
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2958
3015
|
return;
|
|
2959
3016
|
}
|
|
2960
3017
|
const resolved = await fs.resolveAbsolute(root, path);
|
|
2961
3018
|
if (!("ok" in resolved)) {
|
|
2962
|
-
json(res, FAIL(resolved));
|
|
3019
|
+
json$1(res, FAIL$1(resolved));
|
|
2963
3020
|
return;
|
|
2964
3021
|
}
|
|
2965
3022
|
const error = spawnOsCommand(ctx, openArgv(process.platform, resolved.abs));
|
|
2966
|
-
json(res, error === null ? OK({ ok: true }) : FAIL(error));
|
|
3023
|
+
json$1(res, error === null ? OK$1({ ok: true }) : FAIL$1(error));
|
|
2967
3024
|
return;
|
|
2968
3025
|
}
|
|
2969
3026
|
case "/filemgr/rename": {
|
|
2970
3027
|
const path = strField(payload, "path");
|
|
2971
3028
|
const newName = strField(payload, "newName");
|
|
2972
3029
|
if (path === null || newName === null) {
|
|
2973
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3030
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2974
3031
|
return;
|
|
2975
3032
|
}
|
|
2976
3033
|
const result = await fs.rename(root, path, newName);
|
|
2977
|
-
json(res, "ok" in result ? OK(result) : FAIL(result));
|
|
3034
|
+
json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
|
|
2978
3035
|
return;
|
|
2979
3036
|
}
|
|
2980
3037
|
case "/filemgr/mkdir": {
|
|
2981
3038
|
const path = strField(payload, "path");
|
|
2982
3039
|
if (path === null) {
|
|
2983
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3040
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2984
3041
|
return;
|
|
2985
3042
|
}
|
|
2986
3043
|
const result = await fs.mkdir(root, path);
|
|
2987
|
-
json(res, "ok" in result ? OK(result) : FAIL(result));
|
|
3044
|
+
json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
|
|
2988
3045
|
return;
|
|
2989
3046
|
}
|
|
2990
3047
|
case "/filemgr/new-file": {
|
|
2991
3048
|
const path = strField(payload, "path");
|
|
2992
3049
|
if (path === null) {
|
|
2993
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3050
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
2994
3051
|
return;
|
|
2995
3052
|
}
|
|
2996
3053
|
const result = await fs.newFile(root, path);
|
|
2997
|
-
json(res, "ok" in result ? OK(result) : FAIL(result));
|
|
3054
|
+
json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
|
|
2998
3055
|
return;
|
|
2999
3056
|
}
|
|
3000
3057
|
case "/filemgr/git-status": {
|
|
3001
3058
|
const result = await git.status(root);
|
|
3002
|
-
json(res, result === null ? OK(null) : "root" in result ? OK(result) : FAIL(result));
|
|
3059
|
+
json$1(res, result === null ? OK$1(null) : "root" in result ? OK$1(result) : FAIL$1(result));
|
|
3003
3060
|
return;
|
|
3004
3061
|
}
|
|
3005
3062
|
case "/filemgr/git-diff": {
|
|
3006
3063
|
const path = strField(payload, "path");
|
|
3007
3064
|
if (path === null) {
|
|
3008
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3065
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
3009
3066
|
return;
|
|
3010
3067
|
}
|
|
3011
3068
|
const staged = typeof payload === "object" && payload !== null ? payload.staged === true : false;
|
|
3012
3069
|
const result = await git.diff(root, path, staged);
|
|
3013
|
-
json(res, "content" in result ? OK(result) : FAIL(result));
|
|
3070
|
+
json$1(res, "content" in result ? OK$1(result) : FAIL$1(result));
|
|
3014
3071
|
return;
|
|
3015
3072
|
}
|
|
3016
3073
|
case "/filemgr/git-stage": {
|
|
3017
3074
|
const paths = strArray(payload, "paths");
|
|
3018
3075
|
if (paths === null) {
|
|
3019
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3076
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
3020
3077
|
return;
|
|
3021
3078
|
}
|
|
3022
3079
|
const result = await git.stage(root, paths);
|
|
3023
|
-
json(res, "applied" in result ? OK(result) : FAIL(result));
|
|
3080
|
+
json$1(res, "applied" in result ? OK$1(result) : FAIL$1(result));
|
|
3024
3081
|
return;
|
|
3025
3082
|
}
|
|
3026
3083
|
case "/filemgr/git-unstage": {
|
|
3027
3084
|
const paths = strArray(payload, "paths");
|
|
3028
3085
|
if (paths === null) {
|
|
3029
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3086
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
3030
3087
|
return;
|
|
3031
3088
|
}
|
|
3032
3089
|
const result = await git.unstage(root, paths);
|
|
3033
|
-
json(res, "applied" in result ? OK(result) : FAIL(result));
|
|
3090
|
+
json$1(res, "applied" in result ? OK$1(result) : FAIL$1(result));
|
|
3034
3091
|
return;
|
|
3035
3092
|
}
|
|
3036
3093
|
case "/filemgr/git-discard": {
|
|
3037
3094
|
const paths = strArray(payload, "paths");
|
|
3038
3095
|
if (paths === null) {
|
|
3039
|
-
json(res, FAIL(BAD_REQUEST));
|
|
3096
|
+
json$1(res, FAIL$1(BAD_REQUEST));
|
|
3040
3097
|
return;
|
|
3041
3098
|
}
|
|
3042
3099
|
const result = await git.discard(root, paths);
|
|
3043
|
-
json(res, "applied" in result ? OK(result) : FAIL(result));
|
|
3100
|
+
json$1(res, "applied" in result ? OK$1(result) : FAIL$1(result));
|
|
3044
3101
|
return;
|
|
3045
3102
|
}
|
|
3046
3103
|
default:
|
|
@@ -3061,7 +3118,7 @@ function registerPanelRoutes(ctx, fs, git) {
|
|
|
3061
3118
|
}
|
|
3062
3119
|
const gated = await fs.verify(root);
|
|
3063
3120
|
if (!gated.ok) {
|
|
3064
|
-
json(res, FAIL(gated.error), 400);
|
|
3121
|
+
json$1(res, FAIL$1(gated.error), 400);
|
|
3065
3122
|
return;
|
|
3066
3123
|
}
|
|
3067
3124
|
res.writeHead(200, {
|
|
@@ -3078,6 +3135,7 @@ function registerPanelRoutes(ctx, fs, git) {
|
|
|
3078
3135
|
subscribers.add(subscriber);
|
|
3079
3136
|
if (gitUnavailable) push(subscriber, { kind: "gitUnavailable" });
|
|
3080
3137
|
startGitPoll();
|
|
3138
|
+
startFsWatch();
|
|
3081
3139
|
if (heartbeatTimer === void 0) heartbeatTimer = setInterval(() => {
|
|
3082
3140
|
for (const current of subscribers) current.res.write(": ping\n\n");
|
|
3083
3141
|
}, HEARTBEAT_MS);
|
|
@@ -3085,6 +3143,7 @@ function registerPanelRoutes(ctx, fs, git) {
|
|
|
3085
3143
|
subscribers.delete(subscriber);
|
|
3086
3144
|
if (subscribers.size === 0) {
|
|
3087
3145
|
stopGitPoll();
|
|
3146
|
+
stopFsWatch();
|
|
3088
3147
|
if (heartbeatTimer !== void 0) clearInterval(heartbeatTimer);
|
|
3089
3148
|
heartbeatTimer = void 0;
|
|
3090
3149
|
}
|
|
@@ -3102,12 +3161,381 @@ function registerPanelRoutes(ctx, fs, git) {
|
|
|
3102
3161
|
return () => {
|
|
3103
3162
|
for (const dispose of disposers) dispose();
|
|
3104
3163
|
stopGitPoll();
|
|
3164
|
+
stopFsWatch();
|
|
3105
3165
|
if (heartbeatTimer !== void 0) clearInterval(heartbeatTimer);
|
|
3106
3166
|
for (const subscriber of subscribers) subscriber.res.end();
|
|
3107
3167
|
subscribers.clear();
|
|
3108
3168
|
};
|
|
3109
3169
|
}
|
|
3110
3170
|
//#endregion
|
|
3171
|
+
//#region src/host/office-preview.ts
|
|
3172
|
+
const OK = (value) => JSON.stringify({
|
|
3173
|
+
ok: true,
|
|
3174
|
+
value
|
|
3175
|
+
});
|
|
3176
|
+
const FAIL = (error) => JSON.stringify({
|
|
3177
|
+
ok: false,
|
|
3178
|
+
error
|
|
3179
|
+
});
|
|
3180
|
+
/**
|
|
3181
|
+
* Check if officecli is available.
|
|
3182
|
+
* In production (packaged) the binary is bundled via electron-builder extraResources
|
|
3183
|
+
* and injected into PATH by dsh-process.ts addOfficecliPath().
|
|
3184
|
+
* In development we check the vendor/officecli/ directory first, then fall back to PATH.
|
|
3185
|
+
* Cached to avoid repeated probing.
|
|
3186
|
+
*/
|
|
3187
|
+
let officecliAvailable = null;
|
|
3188
|
+
let officecliPath = null;
|
|
3189
|
+
function resolveBundledOfficecli() {
|
|
3190
|
+
const binName = process.platform === "win32" ? "officecli.exe" : "officecli";
|
|
3191
|
+
const os = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux";
|
|
3192
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
3193
|
+
try {
|
|
3194
|
+
const vendorPath = join(fileURLToPath(new URL(".", import.meta.url)), "..", "..", "..", "vendor", "officecli", `${os}-${arch}`, binName);
|
|
3195
|
+
if (existsSync(vendorPath)) return vendorPath;
|
|
3196
|
+
} catch {}
|
|
3197
|
+
return null;
|
|
3198
|
+
}
|
|
3199
|
+
async function probeOfficecli(ctx) {
|
|
3200
|
+
if (officecliAvailable !== null) return officecliAvailable;
|
|
3201
|
+
const bundled = resolveBundledOfficecli();
|
|
3202
|
+
if (bundled !== null) {
|
|
3203
|
+
officecliPath = bundled;
|
|
3204
|
+
officecliAvailable = true;
|
|
3205
|
+
ctx.logger.info(`[dsh-filemgr] officecli found at ${bundled}, OfficeCLI preview enabled`);
|
|
3206
|
+
return true;
|
|
3207
|
+
}
|
|
3208
|
+
const subprocess = ctx.get("subprocess");
|
|
3209
|
+
try {
|
|
3210
|
+
officecliAvailable = (await subprocess.spawn({
|
|
3211
|
+
argv: ["officecli", "--version"],
|
|
3212
|
+
cwd: process.cwd(),
|
|
3213
|
+
stdio: {
|
|
3214
|
+
stdin: "ignore",
|
|
3215
|
+
stdout: { maxBytes: 1024 },
|
|
3216
|
+
stderr: { maxBytes: 1024 }
|
|
3217
|
+
},
|
|
3218
|
+
graceMs: 5e3
|
|
3219
|
+
}).done).exitCode === 0;
|
|
3220
|
+
if (officecliAvailable) {
|
|
3221
|
+
officecliPath = "officecli";
|
|
3222
|
+
ctx.logger.info("[dsh-filemgr] officecli found on PATH, OfficeCLI preview enabled");
|
|
3223
|
+
} else ctx.logger.warn("[dsh-filemgr] officecli not available, OfficeCLI preview disabled");
|
|
3224
|
+
} catch {
|
|
3225
|
+
officecliAvailable = false;
|
|
3226
|
+
ctx.logger.warn("[dsh-filemgr] officecli probe failed, OfficeCLI preview disabled");
|
|
3227
|
+
}
|
|
3228
|
+
return officecliAvailable;
|
|
3229
|
+
}
|
|
3230
|
+
/** Write JSON response. */
|
|
3231
|
+
function json(res, status, body) {
|
|
3232
|
+
res.writeHead(status, {
|
|
3233
|
+
"content-type": "application/json; charset=utf-8",
|
|
3234
|
+
"cache-control": "no-cache"
|
|
3235
|
+
});
|
|
3236
|
+
res.end(body);
|
|
3237
|
+
}
|
|
3238
|
+
/** Write HTML response. */
|
|
3239
|
+
function html(res, body) {
|
|
3240
|
+
res.writeHead(200, {
|
|
3241
|
+
"content-type": "text/html; charset=utf-8",
|
|
3242
|
+
"cache-control": "no-cache"
|
|
3243
|
+
});
|
|
3244
|
+
res.end(body);
|
|
3245
|
+
}
|
|
3246
|
+
/**
|
|
3247
|
+
* Inject a bootstrap that turns OfficeCLI's full-document HTML into a single-
|
|
3248
|
+
* slide browse view that KEEPS the thumbnail sidebar. OfficeCLI ships a
|
|
3249
|
+
* fullscreen engine (`enterFullscreen()` + ←/→/space + thumbnail jumps) but its
|
|
3250
|
+
* `.fullscreen` CSS hides the sidebar (pure presentation mode) — so we:
|
|
3251
|
+
* 1. auto-enter fullscreen after first paint (single page at a time), and
|
|
3252
|
+
* 2. override the fullscreen CSS to restore the sidebar and shrink `.main`
|
|
3253
|
+
* so the thumbnail column stays clickable for page jumps.
|
|
3254
|
+
*/
|
|
3255
|
+
/**
|
|
3256
|
+
* Injected frame/patch add-ons on top of the raw OfficeCLI HTML.
|
|
3257
|
+
*
|
|
3258
|
+
* OfficeCLI colours its chrome (body, sidebar, borders, counter) with hard-coded
|
|
3259
|
+
* dark values and does not follow the dsh light/dark theme. The preview is
|
|
3260
|
+
* rendered inside an isolated blob iframe, so the outer document's
|
|
3261
|
+
* `body[data-ds-dark-theme]` token is NOT visible here — the host sends the
|
|
3262
|
+
* current theme via postMessage and we react by toggling a data attribute that
|
|
3263
|
+
* this stylesheet consumes.
|
|
3264
|
+
*
|
|
3265
|
+
* The slide artwork itself (`.slide`, `.thumb-slide`, `--slide-*`) is authored
|
|
3266
|
+
* by the deck and intentionally left untouched.
|
|
3267
|
+
*/
|
|
3268
|
+
function injectSlideShowMode(htmlContent) {
|
|
3269
|
+
return htmlContent.replace("</body>", `
|
|
3270
|
+
<style data-dsh-officecli-fullscreen>
|
|
3271
|
+
/* Restore the thumbnail sidebar in single-slide (fullscreen) mode and give the
|
|
3272
|
+
slide stage a wider initial viewport by narrowing the sidebar. */
|
|
3273
|
+
body.fullscreen .sidebar { display: flex; width: 140px; min-width: 140px; }
|
|
3274
|
+
body.fullscreen .main { width: calc(100vw - 140px); }
|
|
3275
|
+
body.fullscreen .slide-container { width: 100%; }
|
|
3276
|
+
/* Scale the single slide down so it fits within the stage without being clipped.
|
|
3277
|
+
Using zoom (not transform) because zoom changes both rendering AND layout size,
|
|
3278
|
+
so the parent container adapts naturally. transform:scale() only changes visual
|
|
3279
|
+
size but keeps the original layout footprint, causing overflow clipping. */
|
|
3280
|
+
body.fullscreen .slide {
|
|
3281
|
+
zoom: 0.85;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
/* Light-theme chrome (host sends theme via postMessage; default stays dark). */
|
|
3285
|
+
body[data-dsh-theme="light"] { background: #f7f7f8; }
|
|
3286
|
+
body[data-dsh-theme="light"] .sidebar {
|
|
3287
|
+
background: #eef0f3;
|
|
3288
|
+
border-right: 1px solid rgba(0,0,0,0.09);
|
|
3289
|
+
}
|
|
3290
|
+
body[data-dsh-theme="light"] .sidebar-title,
|
|
3291
|
+
body[data-dsh-theme="light"] .thumb-num,
|
|
3292
|
+
body[data-dsh-theme="light"] .sidebar-toggle { color: #5f6368; }
|
|
3293
|
+
body[data-dsh-theme="light"] .thumb:hover { border-color: #b9bdc6; }
|
|
3294
|
+
body[data-dsh-theme="light"] .page-counter { background: rgba(0,0,0,0.55); color: #f1f3f4; }
|
|
3295
|
+
</style>
|
|
3296
|
+
<script data-dsh-officecli-fullscreen>
|
|
3297
|
+
(function () {
|
|
3298
|
+
// --- single-slide browsing mode (F) ---
|
|
3299
|
+
function boot() {
|
|
3300
|
+
// Only decks carry the fullscreen machinery; skip docs/sheets.
|
|
3301
|
+
if (!document.querySelector('.main .slide-container')) return;
|
|
3302
|
+
if (document.body.classList.contains('fullscreen')) return;
|
|
3303
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F', code: 'KeyF', bubbles: true }));
|
|
3304
|
+
}
|
|
3305
|
+
function onReady(fn) {
|
|
3306
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true });
|
|
3307
|
+
else fn();
|
|
3308
|
+
}
|
|
3309
|
+
onReady(boot);
|
|
3310
|
+
|
|
3311
|
+
// --- follow the host light/dark theme via postMessage (blob iframe isolation) ---
|
|
3312
|
+
window.addEventListener('message', function (event) {
|
|
3313
|
+
if (!event.data || event.data.source !== 'dsh-filemgr' || event.data.type !== 'dsh-theme') return;
|
|
3314
|
+
document.body.setAttribute('data-dsh-theme', event.data.dark ? 'dark' : 'light');
|
|
3315
|
+
});
|
|
3316
|
+
})();
|
|
3317
|
+
<\/script>
|
|
3318
|
+
<style data-dsh-officecli-sheet>
|
|
3319
|
+
.dsh-sheet-selected { outline: 1px solid #2e7df6 !important; outline-offset: -1px; background: rgba(46, 125, 246, 0.12) !important; }
|
|
3320
|
+
</style>
|
|
3321
|
+
<script data-dsh-officecli-sheet-select>
|
|
3322
|
+
(function () {
|
|
3323
|
+
// Excel selection -> host reference. OfficeCLI annotates every cell with
|
|
3324
|
+
// data-path="/<sheet>/<A1>" (column headers use col[A], row headers row[1]).
|
|
3325
|
+
// Some renderers set user-select:none, so a native text selection cannot span
|
|
3326
|
+
// cells; instead we track a pointer drag ourselves (cell under pointerdown ->
|
|
3327
|
+
// cell under the pointer), highlight the bounding rectangle, and postMessage
|
|
3328
|
+
// the sheet + A1 range up. The host turns that into a "引用到对话" chip
|
|
3329
|
+
// (相对路径!Sheet!Range). Docs/slides have no .sheet-content, so this block
|
|
3330
|
+
// is inert for Word/PPT.
|
|
3331
|
+
var MAX_HIGHLIGHT_CELLS = 4000;
|
|
3332
|
+
function isSheetDoc() { return !!document.querySelector('.sheet-content'); }
|
|
3333
|
+
function colToNum(letters) {
|
|
3334
|
+
var n = 0;
|
|
3335
|
+
for (var i = 0; i < letters.length; i++) n = n * 26 + (letters.charCodeAt(i) - 64);
|
|
3336
|
+
return n;
|
|
3337
|
+
}
|
|
3338
|
+
function numToCol(n) {
|
|
3339
|
+
var s = '';
|
|
3340
|
+
while (n > 0) { var r = (n - 1) % 26; s = String.fromCharCode(65 + r) + s; n = (n - r - 1) / 26 | 0; }
|
|
3341
|
+
return s;
|
|
3342
|
+
}
|
|
3343
|
+
function parsePath(el) {
|
|
3344
|
+
var p = el && el.getAttribute && el.getAttribute('data-path'); if (!p) return null;
|
|
3345
|
+
var parts = p.split('/'); // ['', sheet, ref]
|
|
3346
|
+
if (parts.length < 3) return null;
|
|
3347
|
+
var m = /^([A-Z]+)([0-9]+)$/.exec(parts[2]); if (!m) return null;
|
|
3348
|
+
return { sheet: parts[1], col: colToNum(m[1]), row: +m[2] };
|
|
3349
|
+
}
|
|
3350
|
+
// Walk from a DOM node up to the nearest data cell (or null).
|
|
3351
|
+
function pickCell(node) {
|
|
3352
|
+
var el = node;
|
|
3353
|
+
while (el && el !== document) {
|
|
3354
|
+
if (el.nodeType === 1 && el.hasAttribute('data-path')) {
|
|
3355
|
+
var p = parsePath(el); if (p) return p;
|
|
3356
|
+
}
|
|
3357
|
+
el = el.parentElement;
|
|
3358
|
+
}
|
|
3359
|
+
return null;
|
|
3360
|
+
}
|
|
3361
|
+
function clearHighlight() {
|
|
3362
|
+
var els = document.querySelectorAll('.dsh-sheet-selected');
|
|
3363
|
+
for (var i = 0; i < els.length; i++) els[i].classList.remove('dsh-sheet-selected');
|
|
3364
|
+
}
|
|
3365
|
+
function applyHighlight(r) {
|
|
3366
|
+
clearHighlight();
|
|
3367
|
+
if (!r) return;
|
|
3368
|
+
if ((r.c2 - r.c1 + 1) * (r.r2 - r.r1 + 1) > MAX_HIGHLIGHT_CELLS) return; // skip huge ranges
|
|
3369
|
+
var els = document.querySelectorAll('td[data-path], th[data-path]');
|
|
3370
|
+
for (var i = 0; i < els.length; i++) {
|
|
3371
|
+
var p = parsePath(els[i]);
|
|
3372
|
+
if (p && p.sheet === r.sheet && p.row >= r.r1 && p.row <= r.r2 && p.col >= r.c1 && p.col <= r.c2) {
|
|
3373
|
+
els[i].classList.add('dsh-sheet-selected');
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
function send(payload) {
|
|
3378
|
+
parent.postMessage(Object.assign({ source: 'dsh-filemgr', type: 'dsh-sheet-select' }, payload), '*');
|
|
3379
|
+
}
|
|
3380
|
+
function update(from, to) {
|
|
3381
|
+
var refs = [from, to].filter(Boolean);
|
|
3382
|
+
if (!refs.length) { send({ sheet: null, range: null, x: 0, y: 0 }); applyHighlight(null); return; }
|
|
3383
|
+
if (refs[0].sheet !== (refs[1] ? refs[1].sheet : refs[0].sheet)) { send({ sheet: null, range: null, x: 0, y: 0 }); applyHighlight(null); return; }
|
|
3384
|
+
var sheet = refs[0].sheet;
|
|
3385
|
+
var c1 = 1e9, c2 = 0, r1 = 1e9, r2 = 0;
|
|
3386
|
+
refs.forEach(function (p) { c1 = Math.min(c1, p.col); c2 = Math.max(c2, p.col); r1 = Math.min(r1, p.row); r2 = Math.max(r2, p.row); });
|
|
3387
|
+
var corner = numToCol(c1) + r1, corner2 = numToCol(c2) + r2;
|
|
3388
|
+
var range = corner === corner2 ? corner : corner + ':' + corner2;
|
|
3389
|
+
var anchorEl = to ? findCellEl(sheet, refs[1].col, refs[1].row) : findCellEl(sheet, from.col, from.row);
|
|
3390
|
+
var rect = anchorEl ? anchorEl.getBoundingClientRect() : null;
|
|
3391
|
+
applyHighlight({ sheet: sheet, c1: c1, c2: c2, r1: r1, r2: r2 });
|
|
3392
|
+
send({ sheet: sheet, range: range, x: rect ? rect.left : 0, y: rect ? rect.bottom + 6 : 0 });
|
|
3393
|
+
}
|
|
3394
|
+
function findCellEl(sheet, col, row) {
|
|
3395
|
+
var els = document.querySelectorAll('td[data-path], th[data-path]');
|
|
3396
|
+
for (var i = 0; i < els.length; i++) {
|
|
3397
|
+
var p = parsePath(els[i]);
|
|
3398
|
+
if (p && p.sheet === sheet && p.col === col && p.row === row) return els[i];
|
|
3399
|
+
}
|
|
3400
|
+
return null;
|
|
3401
|
+
}
|
|
3402
|
+
var origin = null, dragging = false;
|
|
3403
|
+
document.addEventListener('pointerdown', function (e) {
|
|
3404
|
+
if (!isSheetDoc()) return;
|
|
3405
|
+
var p = pickCell(e.target);
|
|
3406
|
+
if (!p) return; // corner / header / outside — leave selection as-is
|
|
3407
|
+
dragging = true; origin = p; update(p, p);
|
|
3408
|
+
}, true);
|
|
3409
|
+
document.addEventListener('pointermove', function (e) {
|
|
3410
|
+
if (!dragging || !origin) return;
|
|
3411
|
+
var cur = pickCell(document.elementFromPoint(e.clientX, e.clientY));
|
|
3412
|
+
if (cur) update(origin, cur);
|
|
3413
|
+
}, true);
|
|
3414
|
+
window.addEventListener('pointerup', function () {
|
|
3415
|
+
if (!dragging) return;
|
|
3416
|
+
dragging = false; origin = null;
|
|
3417
|
+
}, true);
|
|
3418
|
+
})();
|
|
3419
|
+
<\/script>
|
|
3420
|
+
</body>`);
|
|
3421
|
+
}
|
|
3422
|
+
/** Forbidden (non-loopback request). */
|
|
3423
|
+
function writeForbidden(res) {
|
|
3424
|
+
json(res, 403, FAIL({
|
|
3425
|
+
code: "internal",
|
|
3426
|
+
message: "forbidden: loopback-only"
|
|
3427
|
+
}));
|
|
3428
|
+
}
|
|
3429
|
+
/**
|
|
3430
|
+
* Handle one GET /filemgr/office-preview request.
|
|
3431
|
+
* Query params: root=workspace/root&path=relative/to/root/file.pptx, probe=1 for mtime check.
|
|
3432
|
+
* Uses the same root+path convention as /filemgr/raw.
|
|
3433
|
+
*/
|
|
3434
|
+
async function handleOfficePreview(ctx, req, res) {
|
|
3435
|
+
if (!isLoopbackRequest(req)) {
|
|
3436
|
+
writeForbidden(res);
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
3440
|
+
const root = url.searchParams.get("root");
|
|
3441
|
+
const path = url.searchParams.get("path");
|
|
3442
|
+
if (root === null || root === "" || path === null || path === "") {
|
|
3443
|
+
json(res, 400, FAIL({
|
|
3444
|
+
code: "internal",
|
|
3445
|
+
message: "missing root or path parameter"
|
|
3446
|
+
}));
|
|
3447
|
+
return;
|
|
3448
|
+
}
|
|
3449
|
+
const absPath = join(root, path);
|
|
3450
|
+
if (url.searchParams.get("probe") === "1") {
|
|
3451
|
+
try {
|
|
3452
|
+
const info = await stat(absPath);
|
|
3453
|
+
json(res, 200, OK({ mtime: info.mtimeMs }));
|
|
3454
|
+
} catch {
|
|
3455
|
+
json(res, 404, FAIL({
|
|
3456
|
+
code: "not-found",
|
|
3457
|
+
message: "file not found"
|
|
3458
|
+
}));
|
|
3459
|
+
}
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
if (!await probeOfficecli(ctx) || officecliPath === null) {
|
|
3463
|
+
json(res, 501, FAIL({
|
|
3464
|
+
code: "internal",
|
|
3465
|
+
message: "officecli not installed or not on PATH"
|
|
3466
|
+
}));
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
const subprocess = ctx.get("subprocess");
|
|
3470
|
+
let timeoutMs = 6e4;
|
|
3471
|
+
const controller = new AbortController();
|
|
3472
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3473
|
+
try {
|
|
3474
|
+
const handle = subprocess.spawn({
|
|
3475
|
+
argv: [
|
|
3476
|
+
officecliPath,
|
|
3477
|
+
"view",
|
|
3478
|
+
absPath,
|
|
3479
|
+
"html"
|
|
3480
|
+
],
|
|
3481
|
+
cwd: process.cwd(),
|
|
3482
|
+
stdio: {
|
|
3483
|
+
stdin: "ignore",
|
|
3484
|
+
stdout: { maxBytes: 10 * 1024 * 1024 },
|
|
3485
|
+
stderr: { maxBytes: 1 << 20 }
|
|
3486
|
+
},
|
|
3487
|
+
graceMs: 5e3,
|
|
3488
|
+
signal: controller.signal
|
|
3489
|
+
});
|
|
3490
|
+
const outcome = await handle.done;
|
|
3491
|
+
clearTimeout(timer);
|
|
3492
|
+
if (controller.signal.aborted) {
|
|
3493
|
+
json(res, 504, FAIL({
|
|
3494
|
+
code: "internal",
|
|
3495
|
+
message: "officecli rendering timed out after 60s"
|
|
3496
|
+
}));
|
|
3497
|
+
return;
|
|
3498
|
+
}
|
|
3499
|
+
const readStdout = () => handle.collected?.stdout?.readFrom(0).text ?? "";
|
|
3500
|
+
const readStderr = () => handle.collected?.stderr?.readFrom(0).text ?? "";
|
|
3501
|
+
if (outcome.exitCode !== 0) {
|
|
3502
|
+
const stderr = readStderr();
|
|
3503
|
+
const stdout = readStdout();
|
|
3504
|
+
const error = `officecli exited with code ${String(outcome.exitCode)}\n${stderr}\n${stdout}`.slice(0, 1e3);
|
|
3505
|
+
json(res, 422, FAIL({
|
|
3506
|
+
code: "read-failed",
|
|
3507
|
+
message: error.trim()
|
|
3508
|
+
}));
|
|
3509
|
+
return;
|
|
3510
|
+
}
|
|
3511
|
+
const htmlContent = readStdout();
|
|
3512
|
+
if (htmlContent === "") {
|
|
3513
|
+
json(res, 422, FAIL({
|
|
3514
|
+
code: "read-failed",
|
|
3515
|
+
message: "officecli returned empty output"
|
|
3516
|
+
}));
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
html(res, injectSlideShowMode(htmlContent));
|
|
3520
|
+
} catch (error) {
|
|
3521
|
+
clearTimeout(timer);
|
|
3522
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3523
|
+
json(res, 500, FAIL({
|
|
3524
|
+
code: "internal",
|
|
3525
|
+
message: `officecli invocation failed: ${msg}`
|
|
3526
|
+
}));
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
/** Register the /filemgr/office-preview route on the web server. */
|
|
3530
|
+
function registerOfficePreviewRoute(ctx) {
|
|
3531
|
+
probeOfficecli(ctx);
|
|
3532
|
+
return ctx.webServer.register({
|
|
3533
|
+
kind: "prefix",
|
|
3534
|
+
path: "/filemgr/office-preview",
|
|
3535
|
+
handler: (req, res) => handleOfficePreview(ctx, req, res)
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
//#endregion
|
|
3111
3539
|
//#region src/mount-once.ts
|
|
3112
3540
|
/**
|
|
3113
3541
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
@@ -3181,7 +3609,11 @@ function applyImpl(ctx, config = {}) {
|
|
|
3181
3609
|
if (disposePanel === void 0 && enabled) {
|
|
3182
3610
|
disposePanel = ctx.effect(() => {
|
|
3183
3611
|
const disposeRoutes = registerPanelRoutes(ctx, fs, git);
|
|
3184
|
-
|
|
3612
|
+
const disposeOffice = registerOfficePreviewRoute(ctx);
|
|
3613
|
+
return () => {
|
|
3614
|
+
disposeRoutes();
|
|
3615
|
+
disposeOffice();
|
|
3616
|
+
};
|
|
3185
3617
|
}, "dsh-filemgr: /filemgr routes");
|
|
3186
3618
|
disposePrompt = ctx.effect(() => ctx.systemPrompt.section({
|
|
3187
3619
|
name: "plugin:filemgr",
|