dsh-files-native 0.1.2 → 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 +240 -182
- 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 = [];
|
|
@@ -125,8 +90,8 @@ function subscribe(fn) {
|
|
|
125
90
|
listeners.delete(fn);
|
|
126
91
|
};
|
|
127
92
|
}
|
|
128
|
-
function
|
|
129
|
-
return items.
|
|
93
|
+
function pendingFor(sessionId) {
|
|
94
|
+
return items.filter((item) => item.sessionId === sessionId);
|
|
130
95
|
}
|
|
131
96
|
function add(sessionId, item) {
|
|
132
97
|
items.push({ ...item, sessionId: item.sessionId || sessionId });
|
|
@@ -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() {
|
|
@@ -344,7 +304,7 @@ function uid() {
|
|
|
344
304
|
}
|
|
345
305
|
async function uploadFile(sessionId, file) {
|
|
346
306
|
const id = uid();
|
|
347
|
-
const
|
|
307
|
+
const pending = {
|
|
348
308
|
id,
|
|
349
309
|
sessionId,
|
|
350
310
|
name: file.name,
|
|
@@ -353,14 +313,14 @@ async function uploadFile(sessionId, file) {
|
|
|
353
313
|
mediaType: file.type || "application/octet-stream",
|
|
354
314
|
status: "uploading"
|
|
355
315
|
};
|
|
356
|
-
add(sessionId,
|
|
316
|
+
add(sessionId, pending);
|
|
357
317
|
const url = `/plugins/file-native/upload?sessionId=${encodeURIComponent(sessionId)}&name=${encodeURIComponent(file.name)}&type=${encodeURIComponent(file.type)}`;
|
|
358
318
|
try {
|
|
359
319
|
const res = await fetch(url, { method: "POST", body: file });
|
|
360
320
|
const body = await res.json();
|
|
361
321
|
if (!res.ok || !body.ok || body.file === void 0) {
|
|
362
322
|
patch(sessionId, id, { status: "error", error: body.error ?? "\u4E0A\u4F20\u5931\u8D25" });
|
|
363
|
-
return { ...
|
|
323
|
+
return { ...pending, status: "error", error: body.error ?? "\u4E0A\u4F20\u5931\u8D25" };
|
|
364
324
|
}
|
|
365
325
|
patch(sessionId, id, {
|
|
366
326
|
status: "done",
|
|
@@ -369,10 +329,10 @@ async function uploadFile(sessionId, file) {
|
|
|
369
329
|
size: body.file.size,
|
|
370
330
|
mediaType: body.file.mediaType
|
|
371
331
|
});
|
|
372
|
-
return { ...
|
|
332
|
+
return { ...pending, ...body.file, status: "done" };
|
|
373
333
|
} catch {
|
|
374
334
|
patch(sessionId, id, { status: "error", error: "\u7F51\u7EDC\u9519\u8BEF" });
|
|
375
|
-
return { ...
|
|
335
|
+
return { ...pending, status: "error", error: "\u7F51\u7EDC\u9519\u8BEF" };
|
|
376
336
|
}
|
|
377
337
|
}
|
|
378
338
|
function intake(sessionId, files, onAddImages) {
|
|
@@ -436,9 +396,10 @@ function ImageTile({ item, onRemove, onOpen }) {
|
|
|
436
396
|
}
|
|
437
397
|
function FileTile({ item, sessionId }) {
|
|
438
398
|
const remove2 = () => {
|
|
439
|
-
|
|
399
|
+
const sid = item.sessionId || sessionId;
|
|
400
|
+
remove(sid, item.id);
|
|
440
401
|
if (item.relPath) {
|
|
441
|
-
void fetch(`/plugins/file-native/remove?sessionId=${encodeURIComponent(
|
|
402
|
+
void fetch(`/plugins/file-native/remove?sessionId=${encodeURIComponent(sid)}&path=${encodeURIComponent(item.relPath)}`, { method: "POST" });
|
|
442
403
|
}
|
|
443
404
|
};
|
|
444
405
|
const meta = item.status === "uploading" ? "\u4E0A\u4F20\u4E2D\u2026" : item.status === "error" ? item.error ?? "\u5931\u8D25" : formatSize(item.size);
|
|
@@ -462,7 +423,7 @@ function FileTile({ item, sessionId }) {
|
|
|
462
423
|
function FileRail(props) {
|
|
463
424
|
ensureStyles();
|
|
464
425
|
const sessionId = String(props.sessionId ?? "");
|
|
465
|
-
const [files, setFiles] = (0, import_react2.useState)(() =>
|
|
426
|
+
const [files, setFiles] = (0, import_react2.useState)(() => pendingFor(sessionId));
|
|
466
427
|
const [, setVersion] = (0, import_react2.useState)(() => version());
|
|
467
428
|
const [preview, setPreview] = (0, import_react2.useState)(null);
|
|
468
429
|
const [edges, setEdges] = (0, import_react2.useState)({ left: false, right: false });
|
|
@@ -470,12 +431,12 @@ function FileRail(props) {
|
|
|
470
431
|
const countRef = (0, import_react2.useRef)(0);
|
|
471
432
|
(0, import_react2.useEffect)(() => {
|
|
472
433
|
const sync = () => {
|
|
473
|
-
setFiles(
|
|
434
|
+
setFiles(pendingFor(sessionId));
|
|
474
435
|
setVersion(version());
|
|
475
436
|
};
|
|
476
437
|
sync();
|
|
477
438
|
return subscribe(sync);
|
|
478
|
-
}, []);
|
|
439
|
+
}, [sessionId]);
|
|
479
440
|
const updateEdges = () => {
|
|
480
441
|
const el = rowRef.current;
|
|
481
442
|
if (el === null) return;
|
|
@@ -581,7 +542,9 @@ var import_react_dom2 = require("react-dom");
|
|
|
581
542
|
var owner = null;
|
|
582
543
|
var lastSessionId = "";
|
|
583
544
|
var dragDepth = 0;
|
|
545
|
+
var generation2 = 0;
|
|
584
546
|
var listeners2 = /* @__PURE__ */ new Set();
|
|
547
|
+
var queuedImages = /* @__PURE__ */ new Map();
|
|
585
548
|
function emit2() {
|
|
586
549
|
for (const fn of [...listeners2]) fn();
|
|
587
550
|
}
|
|
@@ -591,14 +554,42 @@ function subscribeLive(fn) {
|
|
|
591
554
|
listeners2.delete(fn);
|
|
592
555
|
};
|
|
593
556
|
}
|
|
557
|
+
function flushQueuedImages() {
|
|
558
|
+
if (!owner?.onAddImages) return;
|
|
559
|
+
const sid = owner.sessionId;
|
|
560
|
+
const files = (queuedImages.get(sid) ?? []).concat(sid ? queuedImages.get("") ?? [] : []);
|
|
561
|
+
queuedImages.delete(sid);
|
|
562
|
+
if (sid) queuedImages.delete("");
|
|
563
|
+
if (files.length === 0) return;
|
|
564
|
+
owner.onAddImages(files);
|
|
565
|
+
}
|
|
594
566
|
function setLiveOwner(next) {
|
|
567
|
+
generation2 += 1;
|
|
595
568
|
owner = next;
|
|
596
569
|
if (next?.sessionId) lastSessionId = next.sessionId;
|
|
597
570
|
emit2();
|
|
571
|
+
if (next) flushQueuedImages();
|
|
572
|
+
return generation2;
|
|
573
|
+
}
|
|
574
|
+
function clearLiveOwner(token) {
|
|
575
|
+
if (token !== generation2) return;
|
|
576
|
+
owner = null;
|
|
577
|
+
emit2();
|
|
598
578
|
}
|
|
599
579
|
function getLiveOwner() {
|
|
600
580
|
return owner;
|
|
601
581
|
}
|
|
582
|
+
function addImages(sessionId, files) {
|
|
583
|
+
if (files.length === 0) return;
|
|
584
|
+
const sid = sessionId || currentSessionId();
|
|
585
|
+
if (owner?.onAddImages && (!owner.sessionId || owner.sessionId === sid)) {
|
|
586
|
+
owner.onAddImages(files);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const key = sid || owner?.sessionId || lastSessionId;
|
|
590
|
+
const prev = queuedImages.get(key) ?? [];
|
|
591
|
+
queuedImages.set(key, prev.concat(files));
|
|
592
|
+
}
|
|
602
593
|
function setDragDepth(depth) {
|
|
603
594
|
const next = Math.max(0, depth);
|
|
604
595
|
if (next === dragDepth) return;
|
|
@@ -696,11 +687,9 @@ function PaperclipButton(props) {
|
|
|
696
687
|
const onChange = (event) => {
|
|
697
688
|
const files = Array.from(event.currentTarget.files ?? []);
|
|
698
689
|
event.currentTarget.value = "";
|
|
699
|
-
const
|
|
700
|
-
const sid = String(props.sessionId || live?.sessionId || currentSessionId());
|
|
690
|
+
const sid = String(props.sessionId || currentSessionId());
|
|
701
691
|
if (files.length === 0) return;
|
|
702
|
-
intakeFiles(sid, files,
|
|
703
|
-
}));
|
|
692
|
+
intakeFiles(sid, files, (images) => addImages(sid, images));
|
|
704
693
|
};
|
|
705
694
|
return (0, import_react5.createElement)(
|
|
706
695
|
"span",
|
|
@@ -809,6 +798,10 @@ function producedPathsOf(owner2) {
|
|
|
809
798
|
// src/context.tsx
|
|
810
799
|
var import_react7 = require("react");
|
|
811
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
|
+
}
|
|
812
805
|
function FileCard({ file, onOpen }) {
|
|
813
806
|
return (0, import_react7.createElement)(
|
|
814
807
|
"button",
|
|
@@ -816,14 +809,14 @@ function FileCard({ file, onOpen }) {
|
|
|
816
809
|
type: "button",
|
|
817
810
|
className: "fr-msg-card",
|
|
818
811
|
title: file.relPath,
|
|
819
|
-
onClick: () => onOpen?.(file
|
|
812
|
+
onClick: () => onOpen?.(file)
|
|
820
813
|
},
|
|
821
814
|
(0, import_react7.createElement)("span", { className: "fr-ext" }, extOf(file.name)),
|
|
822
815
|
(0, import_react7.createElement)(
|
|
823
816
|
"span",
|
|
824
817
|
{ className: "fr-copy" },
|
|
825
818
|
(0, import_react7.createElement)("span", { className: "fr-name" }, file.name),
|
|
826
|
-
(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
|
|
827
820
|
)
|
|
828
821
|
);
|
|
829
822
|
}
|
|
@@ -831,15 +824,11 @@ function userStackOf(el) {
|
|
|
831
824
|
if (!(el instanceof HTMLElement)) return null;
|
|
832
825
|
const kind = el.getAttribute("data-chat-flow-kind");
|
|
833
826
|
if (kind !== "user" && kind !== "steering") return null;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const flow = anchor?.closest('[data-chat-flow-kind="context"]');
|
|
840
|
-
if (flow === null) return null;
|
|
841
|
-
let next = flow.nextElementSibling;
|
|
842
|
-
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;
|
|
843
832
|
for (let hop = 0; hop < 16; hop += 1) {
|
|
844
833
|
const stack = userStackOf(next) ?? userStackOf(prev);
|
|
845
834
|
if (stack !== null) return stack;
|
|
@@ -848,98 +837,106 @@ function findUserStack(anchor) {
|
|
|
848
837
|
}
|
|
849
838
|
return null;
|
|
850
839
|
}
|
|
851
|
-
function
|
|
852
|
-
|
|
853
|
-
if (before === el) return true;
|
|
854
|
-
return el.nextSibling === before;
|
|
855
|
-
}
|
|
856
|
-
function placeHost2(stack) {
|
|
857
|
-
let el = stack.querySelector("[data-file-native-msg]");
|
|
840
|
+
function placeHostInStack(stack) {
|
|
841
|
+
let el = stack.querySelector(":scope > [data-file-native-msg]");
|
|
858
842
|
if (el === null) {
|
|
859
843
|
el = document.createElement("div");
|
|
860
844
|
el.dataset.fileNativeMsg = "";
|
|
861
845
|
el.className = "fr-msg-row";
|
|
846
|
+
stack.insertBefore(el, stack.firstChild);
|
|
862
847
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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);
|
|
868
857
|
}
|
|
869
858
|
return el;
|
|
870
859
|
}
|
|
871
|
-
function
|
|
872
|
-
|
|
873
|
-
const [
|
|
874
|
-
const
|
|
875
|
-
(0, import_react7.
|
|
876
|
-
|
|
877
|
-
if (sid) archiveIfNoticeMatches(sid, props.files);
|
|
878
|
-
}, [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);
|
|
879
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
|
+
}
|
|
880
872
|
let cancelled = false;
|
|
881
|
-
let tries = 0;
|
|
882
873
|
const tick = () => {
|
|
883
874
|
if (cancelled) return;
|
|
884
|
-
const
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
+
});
|
|
892
919
|
};
|
|
893
920
|
tick();
|
|
894
921
|
return () => {
|
|
895
922
|
cancelled = true;
|
|
896
923
|
};
|
|
897
|
-
}, []);
|
|
898
|
-
|
|
899
|
-
"div",
|
|
900
|
-
{ className: "fr-msg-row-inner" },
|
|
901
|
-
...props.files.map((file) => (0, import_react7.createElement)(FileCard, { key: file.relPath, file, onOpen: props.openFile }))
|
|
902
|
-
);
|
|
903
|
-
return (0, import_react7.createElement)("span", {
|
|
904
|
-
ref: anchorRef,
|
|
905
|
-
"data-file-native-anchor": true,
|
|
906
|
-
"aria-hidden": true
|
|
907
|
-
}, host !== null && host.isConnected ? (0, import_react_dom3.createPortal)(gallery, host) : null);
|
|
908
|
-
}
|
|
909
|
-
function OtherContext({ node }) {
|
|
910
|
-
const [open, setOpen] = (0, import_react7.useState)(false);
|
|
911
|
-
const data = node.data ?? {};
|
|
912
|
-
const text = contentText(data.content);
|
|
913
|
-
const summary = data.source?.summary || data.provenance?.label || "\u4E0A\u4E0B\u6587\u6CE8\u5165";
|
|
914
|
-
const source = data.source?.plugin || data.provenance?.label || "";
|
|
924
|
+
}, [props.sessionId]);
|
|
925
|
+
if (hosted.length === 0) return null;
|
|
915
926
|
return (0, import_react7.createElement)(
|
|
916
|
-
"
|
|
917
|
-
{
|
|
918
|
-
(0,
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
(0, import_react7.createElement)("span", { className: "fr-ctx-sum" }, summary)
|
|
928
|
-
),
|
|
929
|
-
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
|
+
))
|
|
930
938
|
);
|
|
931
939
|
}
|
|
932
|
-
function ContextNodeView(props) {
|
|
933
|
-
ensureStyles();
|
|
934
|
-
const node = props.node ?? {};
|
|
935
|
-
const source = node.data?.source;
|
|
936
|
-
if (source?.kind === "plugin" && source.plugin === "file-native") {
|
|
937
|
-
const files = parseNoticeFiles(contentText(node.data?.content));
|
|
938
|
-
if (files.length === 0) return null;
|
|
939
|
-
return (0, import_react7.createElement)(AttachToUserBubble, { files, openFile: props.openFile });
|
|
940
|
-
}
|
|
941
|
-
return (0, import_react7.createElement)(OtherContext, { node });
|
|
942
|
-
}
|
|
943
940
|
|
|
944
941
|
// src/intercept.ts
|
|
945
942
|
function looksLikeFileDrag(transfer) {
|
|
@@ -1031,38 +1028,79 @@ function installFileIntercept(handlers) {
|
|
|
1031
1028
|
|
|
1032
1029
|
// src/client.ts
|
|
1033
1030
|
var name = "file-native";
|
|
1034
|
-
var inject = ["slots"];
|
|
1031
|
+
var inject = ["slots", "betterSidebar"];
|
|
1035
1032
|
function RailSlot(props) {
|
|
1036
1033
|
ensureStyles();
|
|
1037
1034
|
const sessionId = resolveSessionId(props);
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1035
|
+
rememberSessionId(sessionId);
|
|
1036
|
+
const tokenRef = (0, import_react8.useRef)(0);
|
|
1037
|
+
(0, import_react8.useLayoutEffect)(() => {
|
|
1038
|
+
tokenRef.current = setLiveOwner({
|
|
1039
|
+
attachments: props.attachments,
|
|
1040
|
+
canAcceptDrop: props.canAcceptDrop,
|
|
1041
|
+
onAddImages: props.onAddImages,
|
|
1042
|
+
onRemoveImage: props.onRemoveImage,
|
|
1043
|
+
sessionId,
|
|
1044
|
+
dropLimits: props.dropLimits
|
|
1045
|
+
});
|
|
1045
1046
|
});
|
|
1046
|
-
(0, import_react8.
|
|
1047
|
+
(0, import_react8.useLayoutEffect)(() => () => clearLiveOwner(tokenRef.current), []);
|
|
1047
1048
|
return null;
|
|
1048
1049
|
}
|
|
1050
|
+
var chatCardOpener;
|
|
1049
1051
|
function PickerSlot(props) {
|
|
1050
1052
|
const sessionId = resolveSessionId(props);
|
|
1051
1053
|
rememberSessionId(sessionId);
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
const
|
|
1055
|
-
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"}`;
|
|
1056
1058
|
(0, import_react8.useEffect)(() => {
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
+
};
|
|
1074
|
+
(0, import_react8.useEffect)(() => {
|
|
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
|
+
);
|
|
1066
1104
|
}
|
|
1067
1105
|
function TailSlot(props) {
|
|
1068
1106
|
return (0, import_react8.createElement)(UploadedTail, {
|
|
@@ -1082,36 +1120,56 @@ function installLiveIntercept() {
|
|
|
1082
1120
|
canAccept: () => true,
|
|
1083
1121
|
onDepth: (depth) => setDragDepth(depth),
|
|
1084
1122
|
onFiles: (files) => {
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
const addImages = live?.onAddImages ?? (() => {
|
|
1088
|
-
});
|
|
1089
|
-
intakeFiles(sessionId, files, addImages);
|
|
1123
|
+
const sessionId = currentSessionId();
|
|
1124
|
+
intakeFiles(sessionId, files, (images) => addImages(sessionId, images));
|
|
1090
1125
|
}
|
|
1091
1126
|
});
|
|
1092
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
|
+
})();
|
|
1093
1135
|
function apply(ctx) {
|
|
1094
1136
|
ctx.effect(() => installLiveIntercept(), "file-native: document intercept");
|
|
1095
|
-
|
|
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({
|
|
1096
1158
|
name: "conversation.input.attachments",
|
|
1097
|
-
priority:
|
|
1159
|
+
priority: base
|
|
1098
1160
|
}, RailSlot));
|
|
1099
|
-
|
|
1161
|
+
safeInject("conversation.input.left", () => ctx.slots.register({
|
|
1100
1162
|
name: "conversation.input.left",
|
|
1101
1163
|
id: "file-native-picker",
|
|
1102
|
-
order: 0
|
|
1164
|
+
order: 0,
|
|
1165
|
+
priority: base
|
|
1103
1166
|
}, PickerSlot));
|
|
1104
|
-
|
|
1105
|
-
name: "conversation.chat.node",
|
|
1106
|
-
key: "context",
|
|
1107
|
-
priority: -1
|
|
1108
|
-
}, ContextNodeView));
|
|
1109
|
-
ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
1167
|
+
safeInject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
1110
1168
|
name: "conversation.chat.turnTail",
|
|
1111
|
-
|
|
1112
|
-
|
|
1169
|
+
select: selectTail,
|
|
1170
|
+
priority: base
|
|
1113
1171
|
}, TailSlot));
|
|
1114
|
-
|
|
1172
|
+
console.info(`[file-native] client loaded (instance #${INSTANCE})`);
|
|
1115
1173
|
}
|
|
1116
1174
|
|
|
1117
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
|
+
}
|