dsh-files-native 0.1.3 → 0.2.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/README.md +3 -0
- package/lib/client.js +186 -159
- package/lib/index.js +74 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
- **图片**(PNG / JPEG / WebP / GIF)走官方草稿图栏(64px 缩略图,点击灯箱),发送后进模型视觉上下文。
|
|
17
17
|
- **其它文件**落到会话工作区 `.dsh-uploads/`,同一条轨上用 64px 高可读横条卡(扩展名角标 + 文件名 + 大小)。
|
|
18
|
+
- **附件只跟随「立即发送」的消息**:忙碌时回车 = 纯文字排队,文件留在附件轨等下次空闲发送。
|
|
19
|
+
- **气泡附件卡**:消息发出后,文件卡渲染在用户气泡上方(与图片同列右对齐),注入提示行自动隐藏。
|
|
20
|
+
- **点击打开**:点气泡附件卡——装了 [better-sidebar](https://github.com/用户/dsh-better-sidebar) 走侧栏预览;未装则新页签打开(文本/图片/PDF 页签内展示,二进制自动下载)。
|
|
18
21
|
- 发送后文件卡贴在用户气泡栈(与官方图同一列),作业回复不会插在提问和文件中间。
|
|
19
22
|
- 发送成功即清输入框待发轨。回复结尾只画「产物」chip。
|
|
20
23
|
|
package/lib/client.js
CHANGED
|
@@ -43,7 +43,6 @@ var NATIVE_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
43
43
|
var NATIVE_IMAGE_EXT = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif"]);
|
|
44
44
|
var MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
45
45
|
var MAX_FILES_PER_BATCH = 20;
|
|
46
|
-
var UPLOAD_DIR = ".dsh-uploads";
|
|
47
46
|
function isNativeImage(file) {
|
|
48
47
|
const type = (file.type ?? "").toLowerCase();
|
|
49
48
|
if (NATIVE_IMAGE_TYPES.has(type)) return true;
|
|
@@ -62,12 +61,6 @@ function formatSize(bytes) {
|
|
|
62
61
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
63
62
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
64
63
|
}
|
|
65
|
-
function isSafeRelPath(relPath) {
|
|
66
|
-
if (relPath === "" || relPath.startsWith("/") || relPath.startsWith("\\")) return false;
|
|
67
|
-
const parts = relPath.split(/[/\\]/);
|
|
68
|
-
if (parts[0] !== UPLOAD_DIR) return false;
|
|
69
|
-
return parts.every((part) => part !== "" && part !== "." && part !== "..");
|
|
70
|
-
}
|
|
71
64
|
function splitIntake(files) {
|
|
72
65
|
const images = [];
|
|
73
66
|
const others = [];
|
|
@@ -77,34 +70,6 @@ function splitIntake(files) {
|
|
|
77
70
|
}
|
|
78
71
|
return { images, others };
|
|
79
72
|
}
|
|
80
|
-
var NOTICE_LINE = /^- (.+?) — path="([^"]+)" size=(\d+)(?: type="([^"]*)")?/;
|
|
81
|
-
function parseNoticeFiles(text) {
|
|
82
|
-
const files = [];
|
|
83
|
-
for (const line of text.split("\n")) {
|
|
84
|
-
const match = NOTICE_LINE.exec(line.trim());
|
|
85
|
-
if (match === null) continue;
|
|
86
|
-
const relPath = match[2] ?? "";
|
|
87
|
-
if (!isSafeRelPath(relPath)) continue;
|
|
88
|
-
files.push({
|
|
89
|
-
name: match[1] ?? "file",
|
|
90
|
-
relPath,
|
|
91
|
-
size: Number(match[3] ?? 0),
|
|
92
|
-
mediaType: match[4] || "application/octet-stream"
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
return files;
|
|
96
|
-
}
|
|
97
|
-
function contentText(content) {
|
|
98
|
-
if (typeof content === "string") return content;
|
|
99
|
-
if (!Array.isArray(content)) return "";
|
|
100
|
-
const parts = [];
|
|
101
|
-
for (const block of content) {
|
|
102
|
-
if (block && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
|
103
|
-
parts.push(block.text);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return parts.join("\n");
|
|
107
|
-
}
|
|
108
73
|
|
|
109
74
|
// src/store.ts
|
|
110
75
|
var items = [];
|
|
@@ -162,14 +127,6 @@ function archiveSent(sessionId) {
|
|
|
162
127
|
if (files.length > 0) emit();
|
|
163
128
|
return files;
|
|
164
129
|
}
|
|
165
|
-
function archiveIfNoticeMatches(sessionId, notice) {
|
|
166
|
-
if (notice.length === 0) return [];
|
|
167
|
-
const pendingDone = doneFiles(sessionId);
|
|
168
|
-
if (pendingDone.length === 0) return [];
|
|
169
|
-
const paths = new Set(notice.map((file) => file.relPath));
|
|
170
|
-
if (!pendingDone.some((file) => paths.has(file.relPath))) return [];
|
|
171
|
-
return archiveSent(sessionId);
|
|
172
|
-
}
|
|
173
130
|
function lastSent(sessionId) {
|
|
174
131
|
return sentBySession.get(sessionId) ?? [];
|
|
175
132
|
}
|
|
@@ -261,6 +218,9 @@ var FR_CSS = `
|
|
|
261
218
|
.fr-ctx-body{box-sizing:border-box;background:var(--dsw-alias-markdown-code-block);width:calc(100% - 22px);max-height:141px;
|
|
262
219
|
color:var(--dsw-alias-label-tertiary);font:400 11px/16px var(--ds-font-family-code, ui-monospace,monospace);
|
|
263
220
|
border:none;border-radius:8px;margin:4px 0 0 22px;padding:10px 16px 12px 12px;overflow:auto;white-space:pre-wrap}
|
|
221
|
+
[data-file-native-queue]{flex:none;display:inline-flex;align-items:center;gap:2px;height:20px;padding:0 7px;
|
|
222
|
+
border-radius:999px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);
|
|
223
|
+
font:500 11px/20px Inter, var(--dsw-font-family, system-ui);white-space:nowrap;pointer-events:none}
|
|
264
224
|
`;
|
|
265
225
|
var injected = false;
|
|
266
226
|
function ensureStyles() {
|
|
@@ -484,8 +444,7 @@ function FileRail(props) {
|
|
|
484
444
|
const right = el.scrollLeft < el.scrollWidth - el.clientWidth - 1;
|
|
485
445
|
setEdges((prev) => prev.left === left && prev.right === right ? prev : { left, right });
|
|
486
446
|
};
|
|
487
|
-
const
|
|
488
|
-
const itemCount = props.attachments.length + visibleFiles.length;
|
|
447
|
+
const itemCount = props.attachments.length + files.length;
|
|
489
448
|
(0, import_react2.useLayoutEffect)(() => {
|
|
490
449
|
const grew = countRef.current !== 0 && itemCount > countRef.current;
|
|
491
450
|
countRef.current = itemCount;
|
|
@@ -521,7 +480,7 @@ function FileRail(props) {
|
|
|
521
480
|
el.scrollBy({ left: direction * Math.max(el.clientWidth - 64, 200), behavior: "smooth" });
|
|
522
481
|
};
|
|
523
482
|
const hasImages = props.attachments.length > 0;
|
|
524
|
-
const hasFiles =
|
|
483
|
+
const hasFiles = files.length > 0;
|
|
525
484
|
if (!hasImages && !hasFiles) return null;
|
|
526
485
|
return (0, import_react2.createElement)(
|
|
527
486
|
"div",
|
|
@@ -539,7 +498,7 @@ function FileRail(props) {
|
|
|
539
498
|
onRemove: () => props.onRemoveImage(item.id),
|
|
540
499
|
onOpen: () => setPreview(item)
|
|
541
500
|
})),
|
|
542
|
-
...
|
|
501
|
+
...files.map((item) => (0, import_react2.createElement)(FileTile, { key: item.id, item, sessionId }))
|
|
543
502
|
),
|
|
544
503
|
edges.right ? (0, import_react2.createElement)("button", { type: "button", className: "fr-arrow fr-arrow-right", "aria-label": "\u5411\u53F3", onClick: () => page(1) }, (0, import_react2.createElement)(IconChevronRight)) : null
|
|
545
504
|
),
|
|
@@ -629,7 +588,7 @@ function addImages(sessionId, files) {
|
|
|
629
588
|
}
|
|
630
589
|
const key = sid || owner?.sessionId || lastSessionId;
|
|
631
590
|
const prev = queuedImages.get(key) ?? [];
|
|
632
|
-
queuedImages.set(key, prev.concat(
|
|
591
|
+
queuedImages.set(key, prev.concat(files));
|
|
633
592
|
}
|
|
634
593
|
function setDragDepth(depth) {
|
|
635
594
|
const next = Math.max(0, depth);
|
|
@@ -839,6 +798,10 @@ function producedPathsOf(owner2) {
|
|
|
839
798
|
// src/context.tsx
|
|
840
799
|
var import_react7 = require("react");
|
|
841
800
|
var import_react_dom3 = require("react-dom");
|
|
801
|
+
function isFileNoticeRow(el) {
|
|
802
|
+
const text = el.textContent ?? "";
|
|
803
|
+
return text.includes("\u{1F4CE} \u9644\u4EF6") && text.includes("\u4E2A\u6587\u4EF6");
|
|
804
|
+
}
|
|
842
805
|
function FileCard({ file, onOpen }) {
|
|
843
806
|
return (0, import_react7.createElement)(
|
|
844
807
|
"button",
|
|
@@ -846,14 +809,14 @@ function FileCard({ file, onOpen }) {
|
|
|
846
809
|
type: "button",
|
|
847
810
|
className: "fr-msg-card",
|
|
848
811
|
title: file.relPath,
|
|
849
|
-
onClick: () => onOpen?.(file
|
|
812
|
+
onClick: () => onOpen?.(file)
|
|
850
813
|
},
|
|
851
814
|
(0, import_react7.createElement)("span", { className: "fr-ext" }, extOf(file.name)),
|
|
852
815
|
(0, import_react7.createElement)(
|
|
853
816
|
"span",
|
|
854
817
|
{ className: "fr-copy" },
|
|
855
818
|
(0, import_react7.createElement)("span", { className: "fr-name" }, file.name),
|
|
856
|
-
(0, import_react7.createElement)("span", { className: "fr-meta" }, formatSize(file.size))
|
|
819
|
+
file.size > 0 ? (0, import_react7.createElement)("span", { className: "fr-meta" }, formatSize(file.size)) : null
|
|
857
820
|
)
|
|
858
821
|
);
|
|
859
822
|
}
|
|
@@ -861,15 +824,11 @@ function userStackOf(el) {
|
|
|
861
824
|
if (!(el instanceof HTMLElement)) return null;
|
|
862
825
|
const kind = el.getAttribute("data-chat-flow-kind");
|
|
863
826
|
if (kind !== "user" && kind !== "steering") return null;
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const flow = anchor?.closest('[data-chat-flow-kind="context"]');
|
|
870
|
-
if (flow === null) return null;
|
|
871
|
-
let next = flow.nextElementSibling;
|
|
872
|
-
let prev = flow.previousElementSibling;
|
|
827
|
+
return el.querySelector('[class*="userStack"]');
|
|
828
|
+
}
|
|
829
|
+
function findUserStack(notice) {
|
|
830
|
+
let next = notice.nextElementSibling;
|
|
831
|
+
let prev = notice.previousElementSibling;
|
|
873
832
|
for (let hop = 0; hop < 16; hop += 1) {
|
|
874
833
|
const stack = userStackOf(next) ?? userStackOf(prev);
|
|
875
834
|
if (stack !== null) return stack;
|
|
@@ -878,98 +837,106 @@ function findUserStack(anchor) {
|
|
|
878
837
|
}
|
|
879
838
|
return null;
|
|
880
839
|
}
|
|
881
|
-
function
|
|
882
|
-
|
|
883
|
-
if (before === el) return true;
|
|
884
|
-
return el.nextSibling === before;
|
|
885
|
-
}
|
|
886
|
-
function placeHost2(stack) {
|
|
887
|
-
let el = stack.querySelector("[data-file-native-msg]");
|
|
840
|
+
function placeHostInStack(stack) {
|
|
841
|
+
let el = stack.querySelector(":scope > [data-file-native-msg]");
|
|
888
842
|
if (el === null) {
|
|
889
843
|
el = document.createElement("div");
|
|
890
844
|
el.dataset.fileNativeMsg = "";
|
|
891
845
|
el.className = "fr-msg-row";
|
|
846
|
+
stack.insertBefore(el, stack.firstChild);
|
|
892
847
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
848
|
+
return el;
|
|
849
|
+
}
|
|
850
|
+
function placeHostInRow(notice) {
|
|
851
|
+
let el = notice.querySelector(":scope > [data-file-native-msg]");
|
|
852
|
+
if (el === null) {
|
|
853
|
+
el = document.createElement("div");
|
|
854
|
+
el.dataset.fileNativeMsg = "";
|
|
855
|
+
el.style.cssText = "flex:0 0 100%;display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;padding:2px 44px 8px 22px;width:100%;box-sizing:border-box";
|
|
856
|
+
notice.appendChild(el);
|
|
898
857
|
}
|
|
899
858
|
return el;
|
|
900
859
|
}
|
|
901
|
-
function
|
|
902
|
-
|
|
903
|
-
const [
|
|
904
|
-
const
|
|
905
|
-
(0, import_react7.
|
|
906
|
-
|
|
907
|
-
if (sid) archiveIfNoticeMatches(sid, props.files);
|
|
908
|
-
}, [noticeKey]);
|
|
860
|
+
function MessageCards(props) {
|
|
861
|
+
ensureStyles();
|
|
862
|
+
const [hosted, setHosted] = (0, import_react7.useState)([]);
|
|
863
|
+
const sessionKey = (0, import_react7.useRef)("");
|
|
864
|
+
const lastCount = (0, import_react7.useRef)(-1);
|
|
865
|
+
const cachedFiles = (0, import_react7.useRef)(null);
|
|
909
866
|
(0, import_react7.useEffect)(() => {
|
|
867
|
+
if (sessionKey.current !== (props.sessionId ?? "")) {
|
|
868
|
+
sessionKey.current = props.sessionId ?? "";
|
|
869
|
+
lastCount.current = -1;
|
|
870
|
+
cachedFiles.current = null;
|
|
871
|
+
}
|
|
910
872
|
let cancelled = false;
|
|
911
|
-
let tries = 0;
|
|
912
873
|
const tick = () => {
|
|
913
874
|
if (cancelled) return;
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
875
|
+
const run = async () => {
|
|
876
|
+
const rows = [...document.querySelectorAll('[data-chat-flow-kind="context"]')].filter((el) => isFileNoticeRow(el));
|
|
877
|
+
if (rows.length === 0) {
|
|
878
|
+
if (lastCount.current !== 0) {
|
|
879
|
+
lastCount.current = 0;
|
|
880
|
+
setHosted([]);
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (rows.length !== lastCount.current) {
|
|
885
|
+
const sid = props.sessionId ?? "";
|
|
886
|
+
if (sid !== "") {
|
|
887
|
+
try {
|
|
888
|
+
const res = await fetch(`/plugins/file-native/last?sessionId=${encodeURIComponent(sid)}`);
|
|
889
|
+
const body = await res.json();
|
|
890
|
+
cachedFiles.current = Array.isArray(body.files) ? body.files : [];
|
|
891
|
+
} catch {
|
|
892
|
+
cachedFiles.current = cachedFiles.current ?? [];
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
lastCount.current = rows.length;
|
|
896
|
+
}
|
|
897
|
+
const files = cachedFiles.current ?? [];
|
|
898
|
+
if (files.length === 0) return;
|
|
899
|
+
const latest = rows[rows.length - 1];
|
|
900
|
+
const stack = findUserStack(latest);
|
|
901
|
+
const host = stack !== null ? placeHostInStack(stack) : placeHostInRow(latest);
|
|
902
|
+
if (stack !== null) {
|
|
903
|
+
if (latest.querySelector("[data-file-native-anchor]") === null) {
|
|
904
|
+
const anchor = document.createElement("span");
|
|
905
|
+
anchor.dataset.fileNativeAnchor = "";
|
|
906
|
+
anchor.setAttribute("aria-hidden", "true");
|
|
907
|
+
latest.appendChild(anchor);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
setHosted((prev) => {
|
|
911
|
+
if (prev.length === 1 && prev[0].host === host) return prev;
|
|
912
|
+
return [{ host, files }];
|
|
913
|
+
});
|
|
914
|
+
};
|
|
915
|
+
void run().catch(() => {
|
|
916
|
+
}).then(() => {
|
|
917
|
+
if (!cancelled) window.setTimeout(tick, 300);
|
|
918
|
+
});
|
|
922
919
|
};
|
|
923
920
|
tick();
|
|
924
921
|
return () => {
|
|
925
922
|
cancelled = true;
|
|
926
923
|
};
|
|
927
|
-
}, []);
|
|
928
|
-
|
|
929
|
-
"div",
|
|
930
|
-
{ className: "fr-msg-row-inner" },
|
|
931
|
-
...props.files.map((file) => (0, import_react7.createElement)(FileCard, { key: file.relPath, file, onOpen: props.openFile }))
|
|
932
|
-
);
|
|
933
|
-
return (0, import_react7.createElement)("span", {
|
|
934
|
-
ref: anchorRef,
|
|
935
|
-
"data-file-native-anchor": true,
|
|
936
|
-
"aria-hidden": true
|
|
937
|
-
}, host !== null && host.isConnected ? (0, import_react_dom3.createPortal)(gallery, host) : null);
|
|
938
|
-
}
|
|
939
|
-
function OtherContext({ node }) {
|
|
940
|
-
const [open, setOpen] = (0, import_react7.useState)(false);
|
|
941
|
-
const data = node.data ?? {};
|
|
942
|
-
const text = contentText(data.content);
|
|
943
|
-
const summary = data.source?.summary || data.provenance?.label || "\u4E0A\u4E0B\u6587\u6CE8\u5165";
|
|
944
|
-
const source = data.source?.plugin || data.provenance?.label || "";
|
|
924
|
+
}, [props.sessionId]);
|
|
925
|
+
if (hosted.length === 0) return null;
|
|
945
926
|
return (0, import_react7.createElement)(
|
|
946
|
-
"
|
|
947
|
-
{
|
|
948
|
-
(0,
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
(0, import_react7.createElement)("span", { className: "fr-ctx-sum" }, summary)
|
|
958
|
-
),
|
|
959
|
-
open ? (0, import_react7.createElement)("pre", { className: "fr-ctx-body" }, text) : null
|
|
927
|
+
"span",
|
|
928
|
+
{ "aria-hidden": true },
|
|
929
|
+
...hosted.map((item, i) => (0, import_react_dom3.createPortal)(
|
|
930
|
+
(0, import_react7.createElement)(
|
|
931
|
+
"div",
|
|
932
|
+
{ className: "fr-msg-row-inner" },
|
|
933
|
+
...item.files.map((file) => (0, import_react7.createElement)(FileCard, { key: file.relPath, file, onOpen: props.onOpen }))
|
|
934
|
+
),
|
|
935
|
+
item.host,
|
|
936
|
+
`file-native-cards-${i}`
|
|
937
|
+
))
|
|
960
938
|
);
|
|
961
939
|
}
|
|
962
|
-
function ContextNodeView(props) {
|
|
963
|
-
ensureStyles();
|
|
964
|
-
const node = props.node ?? {};
|
|
965
|
-
const source = node.data?.source;
|
|
966
|
-
if (source?.kind === "plugin" && source.plugin === "file-native") {
|
|
967
|
-
const files = parseNoticeFiles(contentText(node.data?.content));
|
|
968
|
-
if (files.length === 0) return null;
|
|
969
|
-
return (0, import_react7.createElement)(AttachToUserBubble, { files, openFile: props.openFile });
|
|
970
|
-
}
|
|
971
|
-
return (0, import_react7.createElement)(OtherContext, { node });
|
|
972
|
-
}
|
|
973
940
|
|
|
974
941
|
// src/intercept.ts
|
|
975
942
|
function looksLikeFileDrag(transfer) {
|
|
@@ -1061,7 +1028,7 @@ function installFileIntercept(handlers) {
|
|
|
1061
1028
|
|
|
1062
1029
|
// src/client.ts
|
|
1063
1030
|
var name = "file-native";
|
|
1064
|
-
var inject = ["slots"];
|
|
1031
|
+
var inject = ["slots", "betterSidebar"];
|
|
1065
1032
|
function RailSlot(props) {
|
|
1066
1033
|
ensureStyles();
|
|
1067
1034
|
const sessionId = resolveSessionId(props);
|
|
@@ -1076,27 +1043,64 @@ function RailSlot(props) {
|
|
|
1076
1043
|
sessionId,
|
|
1077
1044
|
dropLimits: props.dropLimits
|
|
1078
1045
|
});
|
|
1079
|
-
}
|
|
1046
|
+
});
|
|
1080
1047
|
(0, import_react8.useLayoutEffect)(() => () => clearLiveOwner(tokenRef.current), []);
|
|
1081
1048
|
return null;
|
|
1082
1049
|
}
|
|
1050
|
+
var chatCardOpener;
|
|
1083
1051
|
function PickerSlot(props) {
|
|
1084
1052
|
const sessionId = resolveSessionId(props);
|
|
1085
1053
|
rememberSessionId(sessionId);
|
|
1086
|
-
const
|
|
1087
|
-
const
|
|
1088
|
-
const
|
|
1089
|
-
const
|
|
1054
|
+
const running = Boolean(props.useSession?.((state) => state.running));
|
|
1055
|
+
const inputActions = props.inputActions;
|
|
1056
|
+
const submitAction = inputActions?.submit;
|
|
1057
|
+
const kitSig = `inputActions=${props.inputActions ? "y" : "n"} useSession=${props.useSession ? "y" : "n"}`;
|
|
1058
|
+
(0, import_react8.useEffect)(() => {
|
|
1059
|
+
console.info(`[file-native] kit: ${kitSig}`);
|
|
1060
|
+
}, [kitSig]);
|
|
1061
|
+
const submitWithFiles = (ev, sid) => {
|
|
1062
|
+
const submitFn = inputActions?.submit;
|
|
1063
|
+
archiveSent(sid);
|
|
1064
|
+
if (submitFn === void 0) {
|
|
1065
|
+
void fetch(`/plugins/file-native/arm?sessionId=${encodeURIComponent(sid)}`, { method: "POST" }).catch(() => {
|
|
1066
|
+
});
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
ev.preventDefault();
|
|
1070
|
+
ev.stopPropagation();
|
|
1071
|
+
void fetch(`/plugins/file-native/arm?sessionId=${encodeURIComponent(sid)}`, { method: "POST" }).catch(() => {
|
|
1072
|
+
}).then(() => submitFn());
|
|
1073
|
+
};
|
|
1090
1074
|
(0, import_react8.useEffect)(() => {
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1075
|
+
const onKeyDown = (ev) => {
|
|
1076
|
+
if (ev.key !== "Enter" || ev.shiftKey || ev.isComposing || ev.altKey || ev.ctrlKey || ev.metaKey) return;
|
|
1077
|
+
if (running) return;
|
|
1078
|
+
const sid = sessionId || currentSessionId();
|
|
1079
|
+
if (sid === "" || doneFiles(sid).length === 0) return;
|
|
1080
|
+
submitWithFiles(ev, sid);
|
|
1081
|
+
};
|
|
1082
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1083
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
1084
|
+
}, [running, inputActions, sessionId]);
|
|
1085
|
+
(0, import_react8.useEffect)(() => {
|
|
1086
|
+
const onClick = (ev) => {
|
|
1087
|
+
if (running) return;
|
|
1088
|
+
const button = ev.target instanceof Element ? ev.target.closest("button") : null;
|
|
1089
|
+
if (button === null) return;
|
|
1090
|
+
if (!/[a-zA-Z0-9_-]*primary/.test(button.className)) return;
|
|
1091
|
+
const sid = sessionId || currentSessionId();
|
|
1092
|
+
if (sid === "" || doneFiles(sid).length === 0) return;
|
|
1093
|
+
submitWithFiles(ev, sid);
|
|
1094
|
+
};
|
|
1095
|
+
document.addEventListener("click", onClick, true);
|
|
1096
|
+
return () => document.removeEventListener("click", onClick, true);
|
|
1097
|
+
}, [running, inputActions, sessionId]);
|
|
1098
|
+
return (0, import_react8.createElement)(
|
|
1099
|
+
"span",
|
|
1100
|
+
null,
|
|
1101
|
+
(0, import_react8.createElement)(PaperclipButton, { sessionId }),
|
|
1102
|
+
(0, import_react8.createElement)(MessageCards, { sessionId, onOpen: (file) => chatCardOpener?.(sessionId || currentSessionId(), file.relPath, file.name) })
|
|
1103
|
+
);
|
|
1100
1104
|
}
|
|
1101
1105
|
function TailSlot(props) {
|
|
1102
1106
|
return (0, import_react8.createElement)(UploadedTail, {
|
|
@@ -1121,28 +1125,51 @@ function installLiveIntercept() {
|
|
|
1121
1125
|
}
|
|
1122
1126
|
});
|
|
1123
1127
|
}
|
|
1128
|
+
var INSTANCE = (() => {
|
|
1129
|
+
const w = globalThis;
|
|
1130
|
+
w.__fileNativeInstances = (w.__fileNativeInstances ?? 0) + 1;
|
|
1131
|
+
const n = w.__fileNativeInstances;
|
|
1132
|
+
console.warn(`[file-native] apply #${n}`);
|
|
1133
|
+
return n;
|
|
1134
|
+
})();
|
|
1124
1135
|
function apply(ctx) {
|
|
1125
1136
|
ctx.effect(() => installLiveIntercept(), "file-native: document intercept");
|
|
1126
|
-
|
|
1137
|
+
const sidebar = ctx.betterSidebar;
|
|
1138
|
+
if (sidebar !== void 0 && sidebar.features.includes("openFile")) {
|
|
1139
|
+
chatCardOpener = (sid, relPath, name2) => sidebar.openFile({ sessionId: sid }, relPath, name2);
|
|
1140
|
+
} else {
|
|
1141
|
+
chatCardOpener = (sid, relPath) => {
|
|
1142
|
+
window.open(`/plugins/file-native/file?sessionId=${encodeURIComponent(sid)}&path=${encodeURIComponent(relPath)}`, "_blank");
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const base = -(1e3 + INSTANCE * 10);
|
|
1146
|
+
const safeInject = (slot, registerFn) => {
|
|
1147
|
+
ctx.slots.inject(slot, () => {
|
|
1148
|
+
try {
|
|
1149
|
+
return registerFn();
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
console.warn(`[file-native] skip ${slot} registration:`, error);
|
|
1152
|
+
return () => {
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
};
|
|
1157
|
+
safeInject("conversation.input.attachments", () => ctx.slots.register({
|
|
1127
1158
|
name: "conversation.input.attachments",
|
|
1128
|
-
priority:
|
|
1159
|
+
priority: base
|
|
1129
1160
|
}, RailSlot));
|
|
1130
|
-
|
|
1161
|
+
safeInject("conversation.input.left", () => ctx.slots.register({
|
|
1131
1162
|
name: "conversation.input.left",
|
|
1132
1163
|
id: "file-native-picker",
|
|
1133
|
-
order: 0
|
|
1164
|
+
order: 0,
|
|
1165
|
+
priority: base
|
|
1134
1166
|
}, PickerSlot));
|
|
1135
|
-
|
|
1136
|
-
name: "conversation.chat.node",
|
|
1137
|
-
key: "context",
|
|
1138
|
-
priority: -1
|
|
1139
|
-
}, ContextNodeView));
|
|
1140
|
-
ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
1167
|
+
safeInject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
1141
1168
|
name: "conversation.chat.turnTail",
|
|
1142
|
-
|
|
1143
|
-
|
|
1169
|
+
select: selectTail,
|
|
1170
|
+
priority: base
|
|
1144
1171
|
}, TailSlot));
|
|
1145
|
-
|
|
1172
|
+
console.info(`[file-native] client loaded (instance #${INSTANCE})`);
|
|
1146
1173
|
}
|
|
1147
1174
|
|
|
1148
1175
|
return module.exports;
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { createWriteStream } from "node:fs";
|
|
4
|
-
import { mkdir, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
|
5
5
|
import { dirname, join, resolve, sep } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/lib.ts
|
|
@@ -41,6 +41,7 @@ var name = "file-native";
|
|
|
41
41
|
var inject = ["webServer", "sessions"];
|
|
42
42
|
var pending = /* @__PURE__ */ new Map();
|
|
43
43
|
var lastSent = /* @__PURE__ */ new Map();
|
|
44
|
+
var armed = /* @__PURE__ */ new Set();
|
|
44
45
|
function sessionCwd(ctx, sessionId, clientCwd) {
|
|
45
46
|
const headerCwd = ctx.sessions.get(sessionId)?.header?.cwd;
|
|
46
47
|
if (headerCwd) return headerCwd;
|
|
@@ -127,12 +128,12 @@ function registerRoutes(ctx, webServer) {
|
|
|
127
128
|
const bytes = await readBody(req, MAX_FILE_BYTES);
|
|
128
129
|
if (bytes.length === 0) return json(res, 400, { ok: false, error: "empty" });
|
|
129
130
|
const cwd = sessionCwd(ctx, sessionId, q.get("cwd") ?? void 0);
|
|
130
|
-
const
|
|
131
|
-
|
|
131
|
+
const file2 = await writeUpload(cwd, fileName, bytes);
|
|
132
|
+
file2.mediaType = q.get("type") || file2.mediaType;
|
|
132
133
|
const list = pending.get(sessionId) ?? [];
|
|
133
|
-
list.push(
|
|
134
|
+
list.push(file2);
|
|
134
135
|
pending.set(sessionId, list);
|
|
135
|
-
json(res, 200, { ok: true, file });
|
|
136
|
+
json(res, 200, { ok: true, file: file2 });
|
|
136
137
|
} catch (error) {
|
|
137
138
|
const code = error.code === "too-large" ? 413 : 400;
|
|
138
139
|
ctx.logger.warn("[file-native] upload failed: %o", error);
|
|
@@ -168,10 +169,68 @@ function registerRoutes(ctx, webServer) {
|
|
|
168
169
|
json(res, 200, { ok: true, files: lastSent.get(sessionId) ?? [] });
|
|
169
170
|
}
|
|
170
171
|
});
|
|
172
|
+
const arm = webServer.register({
|
|
173
|
+
kind: "exact",
|
|
174
|
+
path: "/plugins/file-native/arm",
|
|
175
|
+
handler: (req, res) => {
|
|
176
|
+
if (!isLoopback(req)) return json(res, 403, { ok: false, error: "forbidden" });
|
|
177
|
+
if (req.method !== "POST") return json(res, 405, { ok: false, error: "method" });
|
|
178
|
+
const sessionId = query(req).get("sessionId") ?? "";
|
|
179
|
+
if (sessionId === "") return json(res, 400, { ok: false, error: "sessionId" });
|
|
180
|
+
armed.add(sessionId);
|
|
181
|
+
json(res, 200, { ok: true });
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
const file = webServer.register({
|
|
185
|
+
kind: "exact",
|
|
186
|
+
path: "/plugins/file-native/file",
|
|
187
|
+
handler: async (req, res) => {
|
|
188
|
+
if (!isLoopback(req)) return json(res, 403, { ok: false, error: "forbidden" });
|
|
189
|
+
if (req.method !== "GET") return json(res, 405, { ok: false, error: "method" });
|
|
190
|
+
const q = query(req);
|
|
191
|
+
const sessionId = q.get("sessionId") ?? "";
|
|
192
|
+
const relPath = q.get("path") ?? "";
|
|
193
|
+
if (sessionId === "" || !isSafeRelPath(relPath)) return json(res, 400, { ok: false, error: "bad-path" });
|
|
194
|
+
try {
|
|
195
|
+
const cwd = sessionCwd(ctx, sessionId);
|
|
196
|
+
const abs = resolve(cwd, relPath);
|
|
197
|
+
const info = await stat(abs);
|
|
198
|
+
if (!info.isFile()) throw new Error("not-file");
|
|
199
|
+
const bytes = await readFile(abs);
|
|
200
|
+
const ext = relPath.split(".").pop()?.toLowerCase() ?? "";
|
|
201
|
+
const types = {
|
|
202
|
+
md: "text/markdown; charset=utf-8",
|
|
203
|
+
txt: "text/plain; charset=utf-8",
|
|
204
|
+
json: "application/json; charset=utf-8",
|
|
205
|
+
csv: "text/csv; charset=utf-8",
|
|
206
|
+
pdf: "application/pdf",
|
|
207
|
+
png: "image/png",
|
|
208
|
+
jpg: "image/jpeg",
|
|
209
|
+
jpeg: "image/jpeg",
|
|
210
|
+
gif: "image/gif",
|
|
211
|
+
webp: "image/webp",
|
|
212
|
+
svg: "image/svg+xml",
|
|
213
|
+
html: "text/html; charset=utf-8",
|
|
214
|
+
xml: "application/xml"
|
|
215
|
+
};
|
|
216
|
+
const name2 = relPath.replace(/^.*[/\\]/, "");
|
|
217
|
+
res.writeHead(200, {
|
|
218
|
+
"content-type": types[ext] ?? "application/octet-stream",
|
|
219
|
+
"content-length": String(bytes.length),
|
|
220
|
+
"content-disposition": `inline; filename*=UTF-8''${encodeURIComponent(name2)}`
|
|
221
|
+
});
|
|
222
|
+
res.end(bytes);
|
|
223
|
+
} catch {
|
|
224
|
+
json(res, 404, { ok: false, error: "not-found" });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
171
228
|
return () => {
|
|
172
229
|
upload();
|
|
173
230
|
remove();
|
|
174
231
|
last();
|
|
232
|
+
arm();
|
|
233
|
+
file();
|
|
175
234
|
};
|
|
176
235
|
}
|
|
177
236
|
function apply(ctx) {
|
|
@@ -205,20 +264,25 @@ function apply(ctx) {
|
|
|
205
264
|
if (decision?.kind === "reject") return decision;
|
|
206
265
|
const agent = payload.agent;
|
|
207
266
|
const sessionId = String(agent?.id ?? "");
|
|
267
|
+
if (!Array.isArray(decision.messages) || decision.messages.length === 0) return decision;
|
|
268
|
+
const lastMessage = decision.messages[decision.messages.length - 1];
|
|
269
|
+
if (String(lastMessage?.role) !== "user") return decision;
|
|
208
270
|
const files = takePending(sessionId);
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
271
|
+
if (!armed.has(sessionId)) return decision;
|
|
272
|
+
armed.delete(sessionId);
|
|
273
|
+
const deliver = files;
|
|
274
|
+
if (deliver.length === 0) return decision;
|
|
275
|
+
lastSent.set(sessionId, deliver);
|
|
212
276
|
const lastIndex = decision.messages.length - 1;
|
|
213
277
|
const notice = {
|
|
214
278
|
id: `file-native-${String(decision.messages[lastIndex]?.id ?? randomUUID())}`,
|
|
215
279
|
role: "user",
|
|
216
|
-
content: [{ type: "text", text: fileListText(
|
|
280
|
+
content: [{ type: "text", text: fileListText(deliver) }],
|
|
217
281
|
source: {
|
|
218
282
|
kind: "plugin",
|
|
219
283
|
plugin: "file-native",
|
|
220
284
|
form: "notice",
|
|
221
|
-
summary: `\u{1F4CE} \u9644\u4EF6 ${
|
|
285
|
+
summary: `\u{1F4CE} \u9644\u4EF6 ${deliver.length} \u4E2A\u6587\u4EF6`
|
|
222
286
|
}
|
|
223
287
|
};
|
|
224
288
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-files-native",
|
|
3
3
|
"description": "DSH 插件:接近原生质感的附件上传——拖入/粘贴/回形针,图片缩略图与文件卡片混排在官方附件栏",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -56,4 +56,4 @@
|
|
|
56
56
|
"esbuild": "^0.28.2",
|
|
57
57
|
"typescript": "^5.6.0"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|