dsh-files-native 0.1.3 → 0.2.1
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 +23 -7
- package/lib/client.js +318 -277
- package/lib/index.js +74 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,12 +11,33 @@
|
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
14
|
+
## ⭐ 欢迎点星收藏
|
|
15
|
+
|
|
16
|
+
如果附件轨帮到了你,欢迎到 [GitHub 仓库](https://github.com/meyaomiao/dsh-files-native) 点个 Star ⭐,让更多 DSH 用户看到它。问题与建议请提 Issue。
|
|
17
|
+
|
|
18
|
+
## 📋 兼容性
|
|
19
|
+
|
|
20
|
+
| 插件版本 | 状态 | 对应 DSH |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| **0.2.x**(当前主线,含 0.2.1) | ✅ | **0.1.5-rc.1 / 0.1.5-rc.2**(及之后的 0.1.5 线) |
|
|
23
|
+
| 0.1.x | 🔧 维护态(仅修 bug) | DSH ≤ 0.1.2-rc.1(仍兼容 0.1.1-rc.2 / 0.1.2-alpha.4) |
|
|
24
|
+
|
|
25
|
+
### 本次升级功能变化
|
|
26
|
+
|
|
27
|
+
- **官方已有的交给官方**:回复尾的「产物」卡不再由本插件绘制,改用 DSH 官方交付文件卡(0.1.5-rc.2 起排版/图标更紧)。用户气泡旁的附件卡仍由本插件画。
|
|
28
|
+
- **粘贴让路**:纯官方四类图(PNG / JPEG / WebP / GIF)交给官方草稿图;其它文件仍走本插件轨。
|
|
29
|
+
- **不依赖 better-sidebar**:模块 `inject` 只有 `slots`。有侧栏则气泡卡走预览,没有则新页签打开。
|
|
30
|
+
- 不要和 `dsh-file-fix` / `dsh-file-upload` / `dsh-paste-to-path` 叠装。
|
|
31
|
+
|
|
14
32
|
运行时插件 id 仍是 `file-native`(上传路由 `/plugins/file-native/*`)。npm / GitHub 仓库名是 `dsh-files-native`。
|
|
15
33
|
|
|
16
34
|
- **图片**(PNG / JPEG / WebP / GIF)走官方草稿图栏(64px 缩略图,点击灯箱),发送后进模型视觉上下文。
|
|
17
35
|
- **其它文件**落到会话工作区 `.dsh-uploads/`,同一条轨上用 64px 高可读横条卡(扩展名角标 + 文件名 + 大小)。
|
|
36
|
+
- **附件只跟随「立即发送」的消息**:忙碌时回车 = 纯文字排队,文件留在附件轨等下次空闲发送。
|
|
37
|
+
- **气泡附件卡**:消息发出后,文件卡渲染在用户气泡上方(与图片同列右对齐),注入提示行自动隐藏。
|
|
38
|
+
- **点击打开**:点气泡附件卡——装了 [better-sidebar](https://github.com/用户/dsh-better-sidebar) 走侧栏预览;未装则新页签打开(文本/图片/PDF 页签内展示,二进制自动下载)。
|
|
18
39
|
- 发送后文件卡贴在用户气泡栈(与官方图同一列),作业回复不会插在提问和文件中间。
|
|
19
|
-
-
|
|
40
|
+
- 发送成功即清输入框待发轨。**回复尾产物卡让给官方。**
|
|
20
41
|
|
|
21
42
|
不要和 `dsh-file-fix` / `dsh-file-upload` / `dsh-paste-to-path` 叠装。
|
|
22
43
|
|
|
@@ -26,7 +47,7 @@
|
|
|
26
47
|
|---|---|
|
|
27
48
|
|  |  |
|
|
28
49
|
|
|
29
|
-
| 拖入窗口任意位置 |
|
|
50
|
+
| 拖入窗口任意位置 | 发送后:图与文件卡跟用户气泡(产物走官方卡) |
|
|
30
51
|
|---|---|
|
|
31
52
|
|  |  |
|
|
32
53
|
|
|
@@ -79,9 +100,4 @@ dsh plugin --profile web add .
|
|
|
79
100
|
- 上传路由仅 loopback。
|
|
80
101
|
- 刷新会丢掉「已上传未发送」的文件卡(磁盘文件仍在 `.dsh-uploads/`)。
|
|
81
102
|
|
|
82
|
-
## 兼容性
|
|
83
|
-
|
|
84
|
-
- DeepSeek Harness `0.1.2-rc.1`(仍兼容 `0.1.1-rc.2` 与 `0.1.2-alpha.4`,web profile)
|
|
85
|
-
- 不要和 `dsh-file-fix` / `dsh-file-upload` / `dsh-paste-to-path` 叠装
|
|
86
|
-
|
|
87
103
|
改仓库前先读 [CONTRIBUTING.md](./CONTRIBUTING.md)(Issue → 分支 → Draft PR)。思考原则见 [AI-ISSUE-WORKFLOW.md](./AI-ISSUE-WORKFLOW.md)。插件硬约束见 [AGENTS.md](./AGENTS.md)。
|
package/lib/client.js
CHANGED
|
@@ -28,7 +28,7 @@ __export(client_exports, {
|
|
|
28
28
|
name: () => name
|
|
29
29
|
});
|
|
30
30
|
module.exports = __toCommonJS(client_exports);
|
|
31
|
-
var
|
|
31
|
+
var import_react7 = require("react");
|
|
32
32
|
|
|
33
33
|
// src/rail.tsx
|
|
34
34
|
var import_react2 = require("react");
|
|
@@ -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,39 +70,30 @@ function splitIntake(files) {
|
|
|
77
70
|
}
|
|
78
71
|
return { images, others };
|
|
79
72
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
for (const
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
}
|
|
73
|
+
function planPaste(files) {
|
|
74
|
+
const nativeImages = [];
|
|
75
|
+
const cards = [];
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
if (isNativeImage(file)) nativeImages.push(file);
|
|
78
|
+
else cards.push(file);
|
|
105
79
|
}
|
|
106
|
-
return
|
|
80
|
+
if (cards.length === 0 && nativeImages.length > 0) return { action: "yield" };
|
|
81
|
+
return { action: "take", cards, nativeImages };
|
|
82
|
+
}
|
|
83
|
+
function extractPasteText(raw, files) {
|
|
84
|
+
const text = raw.replace(/\uFFFC/g, "").trim();
|
|
85
|
+
if (text === "") return "";
|
|
86
|
+
const names = files.map((file) => file.name.replace(/^.*[/\\]/, "")).filter((name2) => name2.length >= 4);
|
|
87
|
+
if (names.length === 0) return text;
|
|
88
|
+
const tokens = text.split(/\s+/).filter(Boolean);
|
|
89
|
+
const referenced = (token) => names.some((name2) => token.toLowerCase().includes(name2.toLowerCase()));
|
|
90
|
+
const meaningful = tokens.filter((token) => !referenced(token));
|
|
91
|
+
return meaningful.length === 0 ? "" : text;
|
|
107
92
|
}
|
|
108
93
|
|
|
109
94
|
// src/store.ts
|
|
110
95
|
var items = [];
|
|
111
96
|
var sentBySession = /* @__PURE__ */ new Map();
|
|
112
|
-
var sentByTurn = /* @__PURE__ */ new Map();
|
|
113
97
|
var listeners = /* @__PURE__ */ new Set();
|
|
114
98
|
var generation = 0;
|
|
115
99
|
function emit() {
|
|
@@ -162,20 +146,6 @@ function archiveSent(sessionId) {
|
|
|
162
146
|
if (files.length > 0) emit();
|
|
163
147
|
return files;
|
|
164
148
|
}
|
|
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
|
-
function lastSent(sessionId) {
|
|
174
|
-
return sentBySession.get(sessionId) ?? [];
|
|
175
|
-
}
|
|
176
|
-
function filesForTurn(sessionId, turn) {
|
|
177
|
-
return sentByTurn.get(`${sessionId}:${turn}`) ?? lastSent(sessionId);
|
|
178
|
-
}
|
|
179
149
|
|
|
180
150
|
// src/styles.ts
|
|
181
151
|
var FR_CSS = `
|
|
@@ -261,6 +231,9 @@ var FR_CSS = `
|
|
|
261
231
|
.fr-ctx-body{box-sizing:border-box;background:var(--dsw-alias-markdown-code-block);width:calc(100% - 22px);max-height:141px;
|
|
262
232
|
color:var(--dsw-alias-label-tertiary);font:400 11px/16px var(--ds-font-family-code, ui-monospace,monospace);
|
|
263
233
|
border:none;border-radius:8px;margin:4px 0 0 22px;padding:10px 16px 12px 12px;overflow:auto;white-space:pre-wrap}
|
|
234
|
+
[data-file-native-queue]{flex:none;display:inline-flex;align-items:center;gap:2px;height:20px;padding:0 7px;
|
|
235
|
+
border-radius:999px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover);
|
|
236
|
+
font:500 11px/20px Inter, var(--dsw-font-family, system-ui);white-space:nowrap;pointer-events:none}
|
|
264
237
|
`;
|
|
265
238
|
var injected = false;
|
|
266
239
|
function ensureStyles() {
|
|
@@ -484,8 +457,7 @@ function FileRail(props) {
|
|
|
484
457
|
const right = el.scrollLeft < el.scrollWidth - el.clientWidth - 1;
|
|
485
458
|
setEdges((prev) => prev.left === left && prev.right === right ? prev : { left, right });
|
|
486
459
|
};
|
|
487
|
-
const
|
|
488
|
-
const itemCount = props.attachments.length + visibleFiles.length;
|
|
460
|
+
const itemCount = props.attachments.length + files.length;
|
|
489
461
|
(0, import_react2.useLayoutEffect)(() => {
|
|
490
462
|
const grew = countRef.current !== 0 && itemCount > countRef.current;
|
|
491
463
|
countRef.current = itemCount;
|
|
@@ -521,7 +493,7 @@ function FileRail(props) {
|
|
|
521
493
|
el.scrollBy({ left: direction * Math.max(el.clientWidth - 64, 200), behavior: "smooth" });
|
|
522
494
|
};
|
|
523
495
|
const hasImages = props.attachments.length > 0;
|
|
524
|
-
const hasFiles =
|
|
496
|
+
const hasFiles = files.length > 0;
|
|
525
497
|
if (!hasImages && !hasFiles) return null;
|
|
526
498
|
return (0, import_react2.createElement)(
|
|
527
499
|
"div",
|
|
@@ -539,7 +511,7 @@ function FileRail(props) {
|
|
|
539
511
|
onRemove: () => props.onRemoveImage(item.id),
|
|
540
512
|
onOpen: () => setPreview(item)
|
|
541
513
|
})),
|
|
542
|
-
...
|
|
514
|
+
...files.map((item) => (0, import_react2.createElement)(FileTile, { key: item.id, item, sessionId }))
|
|
543
515
|
),
|
|
544
516
|
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
517
|
),
|
|
@@ -629,7 +601,7 @@ function addImages(sessionId, files) {
|
|
|
629
601
|
}
|
|
630
602
|
const key = sid || owner?.sessionId || lastSessionId;
|
|
631
603
|
const prev = queuedImages.get(key) ?? [];
|
|
632
|
-
queuedImages.set(key, prev.concat(
|
|
604
|
+
queuedImages.set(key, prev.concat(files));
|
|
633
605
|
}
|
|
634
606
|
function setDragDepth(depth) {
|
|
635
607
|
const next = Math.max(0, depth);
|
|
@@ -717,6 +689,86 @@ function ComposerRail(props) {
|
|
|
717
689
|
}, host !== null && host.isConnected ? (0, import_react_dom2.createPortal)(rail, host) : null);
|
|
718
690
|
}
|
|
719
691
|
|
|
692
|
+
// src/vision.ts
|
|
693
|
+
function currentModelLabel() {
|
|
694
|
+
const buttons = document.querySelectorAll("button[aria-label]");
|
|
695
|
+
for (const button of buttons) {
|
|
696
|
+
const label = button.getAttribute("aria-label") ?? "";
|
|
697
|
+
if (/选择模型|select model|current model/i.test(label)) return label;
|
|
698
|
+
}
|
|
699
|
+
return "";
|
|
700
|
+
}
|
|
701
|
+
function insertComposerText(text) {
|
|
702
|
+
const active = document.activeElement;
|
|
703
|
+
const el = active instanceof HTMLTextAreaElement || active instanceof HTMLInputElement ? active : document.querySelector("[data-composer-card] textarea");
|
|
704
|
+
if (!(el instanceof HTMLTextAreaElement) && !(el instanceof HTMLInputElement)) return;
|
|
705
|
+
el.focus();
|
|
706
|
+
let inserted = false;
|
|
707
|
+
try {
|
|
708
|
+
inserted = document.execCommand("insertText", false, text);
|
|
709
|
+
} catch {
|
|
710
|
+
inserted = false;
|
|
711
|
+
}
|
|
712
|
+
if (inserted) return;
|
|
713
|
+
const proto = el instanceof HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
|
|
714
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
715
|
+
setter?.call(el, `${el.value}${text}`);
|
|
716
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
717
|
+
}
|
|
718
|
+
function pasteAccompanyingText(text) {
|
|
719
|
+
insertComposerText(text);
|
|
720
|
+
}
|
|
721
|
+
var modlensRouteAvailable = true;
|
|
722
|
+
async function probeModlensTakeover() {
|
|
723
|
+
if (!modlensRouteAvailable) return false;
|
|
724
|
+
const label = currentModelLabel();
|
|
725
|
+
try {
|
|
726
|
+
const res = await fetch(`/modlens/paste?model=${encodeURIComponent(label)}`, {
|
|
727
|
+
signal: AbortSignal.timeout(2e3)
|
|
728
|
+
});
|
|
729
|
+
if (res.status === 404) {
|
|
730
|
+
modlensRouteAvailable = false;
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
if (!res.ok) return false;
|
|
734
|
+
const body = await res.json();
|
|
735
|
+
return body.takeover === true;
|
|
736
|
+
} catch {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
async function uploadModlensPaste(file) {
|
|
741
|
+
const res = await fetch("/modlens/paste", { method: "POST", body: await file.arrayBuffer() });
|
|
742
|
+
if (!res.ok) return void 0;
|
|
743
|
+
const body = await res.json();
|
|
744
|
+
return typeof body.path === "string" && body.path !== "" ? body.path : void 0;
|
|
745
|
+
}
|
|
746
|
+
async function deliverImages(sessionId, files) {
|
|
747
|
+
if (files.length === 0) return;
|
|
748
|
+
const takeover = await probeModlensTakeover();
|
|
749
|
+
if (!takeover) {
|
|
750
|
+
addImages(sessionId, files);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const paths = [];
|
|
754
|
+
const failed = [];
|
|
755
|
+
for (const file of files) {
|
|
756
|
+
try {
|
|
757
|
+
const path = await uploadModlensPaste(file);
|
|
758
|
+
if (path !== void 0) paths.push(path);
|
|
759
|
+
else failed.push(file);
|
|
760
|
+
} catch {
|
|
761
|
+
failed.push(file);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (paths.length === 0) {
|
|
765
|
+
addImages(sessionId, files);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (failed.length > 0) addImages(sessionId, failed);
|
|
769
|
+
insertComposerText(`${paths.join(" ")} `);
|
|
770
|
+
}
|
|
771
|
+
|
|
720
772
|
// src/picker.tsx
|
|
721
773
|
function PaperclipButton(props) {
|
|
722
774
|
ensureStyles();
|
|
@@ -730,7 +782,9 @@ function PaperclipButton(props) {
|
|
|
730
782
|
event.currentTarget.value = "";
|
|
731
783
|
const sid = String(props.sessionId || currentSessionId());
|
|
732
784
|
if (files.length === 0) return;
|
|
733
|
-
intakeFiles(sid, files, (images) =>
|
|
785
|
+
intakeFiles(sid, files, (images) => {
|
|
786
|
+
void deliverImages(sid, images);
|
|
787
|
+
});
|
|
734
788
|
};
|
|
735
789
|
return (0, import_react5.createElement)(
|
|
736
790
|
"span",
|
|
@@ -754,106 +808,28 @@ function PaperclipButton(props) {
|
|
|
754
808
|
);
|
|
755
809
|
}
|
|
756
810
|
|
|
757
|
-
// src/tail.tsx
|
|
758
|
-
var import_react6 = require("react");
|
|
759
|
-
function basename(path) {
|
|
760
|
-
const at = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
761
|
-
return at === -1 ? path : path.slice(at + 1);
|
|
762
|
-
}
|
|
763
|
-
function ChipRow({ label, items: items2, open }) {
|
|
764
|
-
if (items2.length === 0) return null;
|
|
765
|
-
const shown = items2.slice(0, 6);
|
|
766
|
-
const hidden = items2.length - shown.length;
|
|
767
|
-
return (0, import_react6.createElement)(
|
|
768
|
-
"div",
|
|
769
|
-
{ className: "fr-tail" },
|
|
770
|
-
(0, import_react6.createElement)("span", { className: "fr-tail-label" }, label),
|
|
771
|
-
(0, import_react6.createElement)(
|
|
772
|
-
"div",
|
|
773
|
-
{ className: "fr-tail-row" },
|
|
774
|
-
...shown.map((item) => (0, import_react6.createElement)("button", {
|
|
775
|
-
key: item.path,
|
|
776
|
-
type: "button",
|
|
777
|
-
className: "fr-chip",
|
|
778
|
-
title: item.path,
|
|
779
|
-
onClick: () => open(item.path)
|
|
780
|
-
}, item.name)),
|
|
781
|
-
hidden > 0 ? (0, import_react6.createElement)("span", { className: "fr-more" }, `+ ${hidden} \u4E2A\u6587\u4EF6`) : null
|
|
782
|
-
)
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
function UploadedTail(props) {
|
|
786
|
-
ensureStyles();
|
|
787
|
-
const sessionId = String(props.sessionId ?? "");
|
|
788
|
-
const [uploaded, setUploaded] = (0, import_react6.useState)(
|
|
789
|
-
props.matched?.uploaded ?? (props.turn !== void 0 ? filesForTurn(sessionId, props.turn) : lastSent(sessionId))
|
|
790
|
-
);
|
|
791
|
-
(0, import_react6.useEffect)(() => {
|
|
792
|
-
if (props.matched?.uploaded) {
|
|
793
|
-
setUploaded(props.matched.uploaded);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
let cancelled = false;
|
|
797
|
-
void fetch(`/plugins/file-native/last?sessionId=${encodeURIComponent(sessionId)}`).then((res) => res.json()).then((body) => {
|
|
798
|
-
if (!cancelled && Array.isArray(body.files) && body.files.length > 0) setUploaded(body.files);
|
|
799
|
-
}).catch(() => {
|
|
800
|
-
});
|
|
801
|
-
return () => {
|
|
802
|
-
cancelled = true;
|
|
803
|
-
};
|
|
804
|
-
}, [sessionId, props.matched, props.turn]);
|
|
805
|
-
const produced = props.matched?.produced ?? [];
|
|
806
|
-
if (uploaded.length === 0 && produced.length === 0) return null;
|
|
807
|
-
const open = props.openFile ?? ((path) => {
|
|
808
|
-
window.open(path, "_blank");
|
|
809
|
-
});
|
|
810
|
-
return (0, import_react6.createElement)(
|
|
811
|
-
"div",
|
|
812
|
-
{ "data-file-native-tail": true },
|
|
813
|
-
(0, import_react6.createElement)(ChipRow, {
|
|
814
|
-
label: "\u9644\u4EF6",
|
|
815
|
-
items: uploaded.map((file) => ({ name: file.name, path: file.relPath })),
|
|
816
|
-
open
|
|
817
|
-
}),
|
|
818
|
-
(0, import_react6.createElement)(ChipRow, {
|
|
819
|
-
label: "\u4EA7\u7269",
|
|
820
|
-
items: produced.map((path) => ({ name: basename(path), path })),
|
|
821
|
-
open
|
|
822
|
-
})
|
|
823
|
-
);
|
|
824
|
-
}
|
|
825
|
-
function producedPathsOf(owner2) {
|
|
826
|
-
const data = owner2.turn?.data?.get?.("deliverables");
|
|
827
|
-
const seq = owner2.seq ?? Number.POSITIVE_INFINITY;
|
|
828
|
-
if (!data?.produced) return [];
|
|
829
|
-
const paths = [];
|
|
830
|
-
const seen = /* @__PURE__ */ new Set();
|
|
831
|
-
for (const item of data.produced) {
|
|
832
|
-
if (item.seq > seq || seen.has(item.path)) continue;
|
|
833
|
-
seen.add(item.path);
|
|
834
|
-
paths.push(item.path);
|
|
835
|
-
}
|
|
836
|
-
return paths;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
811
|
// src/context.tsx
|
|
840
|
-
var
|
|
812
|
+
var import_react6 = require("react");
|
|
841
813
|
var import_react_dom3 = require("react-dom");
|
|
814
|
+
function isFileNoticeRow(el) {
|
|
815
|
+
const text = el.textContent ?? "";
|
|
816
|
+
return text.includes("\u{1F4CE} \u9644\u4EF6") && text.includes("\u4E2A\u6587\u4EF6");
|
|
817
|
+
}
|
|
842
818
|
function FileCard({ file, onOpen }) {
|
|
843
|
-
return (0,
|
|
819
|
+
return (0, import_react6.createElement)(
|
|
844
820
|
"button",
|
|
845
821
|
{
|
|
846
822
|
type: "button",
|
|
847
823
|
className: "fr-msg-card",
|
|
848
824
|
title: file.relPath,
|
|
849
|
-
onClick: () => onOpen?.(file
|
|
825
|
+
onClick: () => onOpen?.(file)
|
|
850
826
|
},
|
|
851
|
-
(0,
|
|
852
|
-
(0,
|
|
827
|
+
(0, import_react6.createElement)("span", { className: "fr-ext" }, extOf(file.name)),
|
|
828
|
+
(0, import_react6.createElement)(
|
|
853
829
|
"span",
|
|
854
830
|
{ className: "fr-copy" },
|
|
855
|
-
(0,
|
|
856
|
-
(0,
|
|
831
|
+
(0, import_react6.createElement)("span", { className: "fr-name" }, file.name),
|
|
832
|
+
file.size > 0 ? (0, import_react6.createElement)("span", { className: "fr-meta" }, formatSize(file.size)) : null
|
|
857
833
|
)
|
|
858
834
|
);
|
|
859
835
|
}
|
|
@@ -861,15 +837,11 @@ function userStackOf(el) {
|
|
|
861
837
|
if (!(el instanceof HTMLElement)) return null;
|
|
862
838
|
const kind = el.getAttribute("data-chat-flow-kind");
|
|
863
839
|
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;
|
|
840
|
+
return el.querySelector('[class*="userStack"]');
|
|
841
|
+
}
|
|
842
|
+
function findUserStack(notice) {
|
|
843
|
+
let next = notice.nextElementSibling;
|
|
844
|
+
let prev = notice.previousElementSibling;
|
|
873
845
|
for (let hop = 0; hop < 16; hop += 1) {
|
|
874
846
|
const stack = userStackOf(next) ?? userStackOf(prev);
|
|
875
847
|
if (stack !== null) return stack;
|
|
@@ -878,98 +850,106 @@ function findUserStack(anchor) {
|
|
|
878
850
|
}
|
|
879
851
|
return null;
|
|
880
852
|
}
|
|
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]");
|
|
853
|
+
function placeHostInStack(stack) {
|
|
854
|
+
let el = stack.querySelector(":scope > [data-file-native-msg]");
|
|
888
855
|
if (el === null) {
|
|
889
856
|
el = document.createElement("div");
|
|
890
857
|
el.dataset.fileNativeMsg = "";
|
|
891
858
|
el.className = "fr-msg-row";
|
|
859
|
+
stack.insertBefore(el, stack.firstChild);
|
|
892
860
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
861
|
+
return el;
|
|
862
|
+
}
|
|
863
|
+
function placeHostInRow(notice) {
|
|
864
|
+
let el = notice.querySelector(":scope > [data-file-native-msg]");
|
|
865
|
+
if (el === null) {
|
|
866
|
+
el = document.createElement("div");
|
|
867
|
+
el.dataset.fileNativeMsg = "";
|
|
868
|
+
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";
|
|
869
|
+
notice.appendChild(el);
|
|
898
870
|
}
|
|
899
871
|
return el;
|
|
900
872
|
}
|
|
901
|
-
function
|
|
902
|
-
|
|
903
|
-
const [
|
|
904
|
-
const
|
|
905
|
-
(0,
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
873
|
+
function MessageCards(props) {
|
|
874
|
+
ensureStyles();
|
|
875
|
+
const [hosted, setHosted] = (0, import_react6.useState)([]);
|
|
876
|
+
const sessionKey = (0, import_react6.useRef)("");
|
|
877
|
+
const lastCount = (0, import_react6.useRef)(-1);
|
|
878
|
+
const cachedFiles = (0, import_react6.useRef)(null);
|
|
879
|
+
(0, import_react6.useEffect)(() => {
|
|
880
|
+
if (sessionKey.current !== (props.sessionId ?? "")) {
|
|
881
|
+
sessionKey.current = props.sessionId ?? "";
|
|
882
|
+
lastCount.current = -1;
|
|
883
|
+
cachedFiles.current = null;
|
|
884
|
+
}
|
|
910
885
|
let cancelled = false;
|
|
911
|
-
let tries = 0;
|
|
912
886
|
const tick = () => {
|
|
913
887
|
if (cancelled) return;
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
888
|
+
const run = async () => {
|
|
889
|
+
const rows = [...document.querySelectorAll('[data-chat-flow-kind="context"]')].filter((el) => isFileNoticeRow(el));
|
|
890
|
+
if (rows.length === 0) {
|
|
891
|
+
if (lastCount.current !== 0) {
|
|
892
|
+
lastCount.current = 0;
|
|
893
|
+
setHosted([]);
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (rows.length !== lastCount.current) {
|
|
898
|
+
const sid = props.sessionId ?? "";
|
|
899
|
+
if (sid !== "") {
|
|
900
|
+
try {
|
|
901
|
+
const res = await fetch(`/plugins/file-native/last?sessionId=${encodeURIComponent(sid)}`);
|
|
902
|
+
const body = await res.json();
|
|
903
|
+
cachedFiles.current = Array.isArray(body.files) ? body.files : [];
|
|
904
|
+
} catch {
|
|
905
|
+
cachedFiles.current = cachedFiles.current ?? [];
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
lastCount.current = rows.length;
|
|
909
|
+
}
|
|
910
|
+
const files = cachedFiles.current ?? [];
|
|
911
|
+
if (files.length === 0) return;
|
|
912
|
+
const latest = rows[rows.length - 1];
|
|
913
|
+
const stack = findUserStack(latest);
|
|
914
|
+
const host = stack !== null ? placeHostInStack(stack) : placeHostInRow(latest);
|
|
915
|
+
if (stack !== null) {
|
|
916
|
+
if (latest.querySelector("[data-file-native-anchor]") === null) {
|
|
917
|
+
const anchor = document.createElement("span");
|
|
918
|
+
anchor.dataset.fileNativeAnchor = "";
|
|
919
|
+
anchor.setAttribute("aria-hidden", "true");
|
|
920
|
+
latest.appendChild(anchor);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
setHosted((prev) => {
|
|
924
|
+
if (prev.length === 1 && prev[0].host === host) return prev;
|
|
925
|
+
return [{ host, files }];
|
|
926
|
+
});
|
|
927
|
+
};
|
|
928
|
+
void run().catch(() => {
|
|
929
|
+
}).then(() => {
|
|
930
|
+
if (!cancelled) window.setTimeout(tick, 300);
|
|
931
|
+
});
|
|
922
932
|
};
|
|
923
933
|
tick();
|
|
924
934
|
return () => {
|
|
925
935
|
cancelled = true;
|
|
926
936
|
};
|
|
927
|
-
}, []);
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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 || "";
|
|
945
|
-
return (0, import_react7.createElement)(
|
|
946
|
-
"div",
|
|
947
|
-
{ className: "fr-ctx", "data-open": open || void 0 },
|
|
948
|
-
(0, import_react7.createElement)(
|
|
949
|
-
"button",
|
|
950
|
-
{
|
|
951
|
-
type: "button",
|
|
952
|
-
className: "fr-ctx-head",
|
|
953
|
-
onClick: () => setOpen((value) => !value)
|
|
954
|
-
},
|
|
955
|
-
(0, import_react7.createElement)("span", { className: "fr-ctx-title" }, "\u4E0A\u4E0B\u6587\u6CE8\u5165"),
|
|
956
|
-
source ? (0, import_react7.createElement)("span", { className: "fr-ctx-src" }, source) : null,
|
|
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
|
|
937
|
+
}, [props.sessionId]);
|
|
938
|
+
if (hosted.length === 0) return null;
|
|
939
|
+
return (0, import_react6.createElement)(
|
|
940
|
+
"span",
|
|
941
|
+
{ "aria-hidden": true },
|
|
942
|
+
...hosted.map((item, i) => (0, import_react_dom3.createPortal)(
|
|
943
|
+
(0, import_react6.createElement)(
|
|
944
|
+
"div",
|
|
945
|
+
{ className: "fr-msg-row-inner" },
|
|
946
|
+
...item.files.map((file) => (0, import_react6.createElement)(FileCard, { key: file.relPath, file, onOpen: props.onOpen }))
|
|
947
|
+
),
|
|
948
|
+
item.host,
|
|
949
|
+
`file-native-cards-${i}`
|
|
950
|
+
))
|
|
960
951
|
);
|
|
961
952
|
}
|
|
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
953
|
|
|
974
954
|
// src/intercept.ts
|
|
975
955
|
function looksLikeFileDrag(transfer) {
|
|
@@ -1039,9 +1019,16 @@ function installFileIntercept(handlers) {
|
|
|
1039
1019
|
const fromItems = Array.from(event.clipboardData?.items ?? []).filter((item) => item.kind === "file").map((item) => item.getAsFile()).filter((file) => file !== null);
|
|
1040
1020
|
const files = fromList.length > 0 ? fromList : fromItems;
|
|
1041
1021
|
if (files.length === 0) return;
|
|
1022
|
+
const plan = planPaste(files);
|
|
1023
|
+
if (plan.action === "yield") return;
|
|
1042
1024
|
event.preventDefault();
|
|
1043
|
-
event.
|
|
1044
|
-
handlers.onFiles(
|
|
1025
|
+
event.stopImmediatePropagation();
|
|
1026
|
+
if (plan.cards.length > 0) handlers.onFiles(plan.cards);
|
|
1027
|
+
if (plan.nativeImages.length > 0) handlers.onImages?.(plan.nativeImages);
|
|
1028
|
+
if (handlers.onText !== void 0) {
|
|
1029
|
+
const kept = extractPasteText(event.clipboardData?.getData("text/plain") ?? "", files);
|
|
1030
|
+
if (kept !== "") handlers.onText(kept);
|
|
1031
|
+
}
|
|
1045
1032
|
};
|
|
1046
1033
|
document.addEventListener("dragenter", onDragEnter, true);
|
|
1047
1034
|
document.addEventListener("dragover", onDragOver, true);
|
|
@@ -1066,8 +1053,8 @@ function RailSlot(props) {
|
|
|
1066
1053
|
ensureStyles();
|
|
1067
1054
|
const sessionId = resolveSessionId(props);
|
|
1068
1055
|
rememberSessionId(sessionId);
|
|
1069
|
-
const tokenRef = (0,
|
|
1070
|
-
(0,
|
|
1056
|
+
const tokenRef = (0, import_react7.useRef)(0);
|
|
1057
|
+
(0, import_react7.useLayoutEffect)(() => {
|
|
1071
1058
|
tokenRef.current = setLiveOwner({
|
|
1072
1059
|
attachments: props.attachments,
|
|
1073
1060
|
canAcceptDrop: props.canAcceptDrop,
|
|
@@ -1076,40 +1063,64 @@ function RailSlot(props) {
|
|
|
1076
1063
|
sessionId,
|
|
1077
1064
|
dropLimits: props.dropLimits
|
|
1078
1065
|
});
|
|
1079
|
-
}
|
|
1080
|
-
(0,
|
|
1066
|
+
});
|
|
1067
|
+
(0, import_react7.useLayoutEffect)(() => () => clearLiveOwner(tokenRef.current), []);
|
|
1081
1068
|
return null;
|
|
1082
1069
|
}
|
|
1070
|
+
var chatCardOpener;
|
|
1083
1071
|
function PickerSlot(props) {
|
|
1084
1072
|
const sessionId = resolveSessionId(props);
|
|
1085
1073
|
rememberSessionId(sessionId);
|
|
1086
|
-
const
|
|
1087
|
-
const
|
|
1088
|
-
const
|
|
1089
|
-
const
|
|
1090
|
-
(0,
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
if (
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
sessionId:
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1074
|
+
const running = Boolean(props.useSession?.((state) => state.running));
|
|
1075
|
+
const inputActions = props.inputActions;
|
|
1076
|
+
const submitAction = inputActions?.submit;
|
|
1077
|
+
const kitSig = `inputActions=${props.inputActions ? "y" : "n"} useSession=${props.useSession ? "y" : "n"}`;
|
|
1078
|
+
(0, import_react7.useEffect)(() => {
|
|
1079
|
+
console.info(`[file-native] kit: ${kitSig}`);
|
|
1080
|
+
}, [kitSig]);
|
|
1081
|
+
const submitWithFiles = (ev, sid) => {
|
|
1082
|
+
const submitFn = inputActions?.submit;
|
|
1083
|
+
archiveSent(sid);
|
|
1084
|
+
if (submitFn === void 0) {
|
|
1085
|
+
void fetch(`/plugins/file-native/arm?sessionId=${encodeURIComponent(sid)}`, { method: "POST" }).catch(() => {
|
|
1086
|
+
});
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
ev.preventDefault();
|
|
1090
|
+
ev.stopPropagation();
|
|
1091
|
+
void fetch(`/plugins/file-native/arm?sessionId=${encodeURIComponent(sid)}`, { method: "POST" }).catch(() => {
|
|
1092
|
+
}).then(() => submitFn());
|
|
1093
|
+
};
|
|
1094
|
+
(0, import_react7.useEffect)(() => {
|
|
1095
|
+
const onKeyDown = (ev) => {
|
|
1096
|
+
if (ev.key !== "Enter" || ev.shiftKey || ev.isComposing || ev.altKey || ev.ctrlKey || ev.metaKey) return;
|
|
1097
|
+
if (running) return;
|
|
1098
|
+
const sid = sessionId || currentSessionId();
|
|
1099
|
+
if (sid === "" || doneFiles(sid).length === 0) return;
|
|
1100
|
+
submitWithFiles(ev, sid);
|
|
1101
|
+
};
|
|
1102
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
1103
|
+
return () => document.removeEventListener("keydown", onKeyDown, true);
|
|
1104
|
+
}, [running, inputActions, sessionId]);
|
|
1105
|
+
(0, import_react7.useEffect)(() => {
|
|
1106
|
+
const onClick = (ev) => {
|
|
1107
|
+
if (running) return;
|
|
1108
|
+
const button = ev.target instanceof Element ? ev.target.closest("button") : null;
|
|
1109
|
+
if (button === null) return;
|
|
1110
|
+
if (!/[a-zA-Z0-9_-]*primary/.test(button.className)) return;
|
|
1111
|
+
const sid = sessionId || currentSessionId();
|
|
1112
|
+
if (sid === "" || doneFiles(sid).length === 0) return;
|
|
1113
|
+
submitWithFiles(ev, sid);
|
|
1114
|
+
};
|
|
1115
|
+
document.addEventListener("click", onClick, true);
|
|
1116
|
+
return () => document.removeEventListener("click", onClick, true);
|
|
1117
|
+
}, [running, inputActions, sessionId]);
|
|
1118
|
+
return (0, import_react7.createElement)(
|
|
1119
|
+
"span",
|
|
1120
|
+
null,
|
|
1121
|
+
(0, import_react7.createElement)(PaperclipButton, { sessionId }),
|
|
1122
|
+
(0, import_react7.createElement)(MessageCards, { sessionId, onOpen: (file) => chatCardOpener?.(sessionId || currentSessionId(), file.relPath, file.name) })
|
|
1123
|
+
);
|
|
1113
1124
|
}
|
|
1114
1125
|
function installLiveIntercept() {
|
|
1115
1126
|
return installFileIntercept({
|
|
@@ -1117,32 +1128,62 @@ function installLiveIntercept() {
|
|
|
1117
1128
|
onDepth: (depth) => setDragDepth(depth),
|
|
1118
1129
|
onFiles: (files) => {
|
|
1119
1130
|
const sessionId = currentSessionId();
|
|
1120
|
-
intakeFiles(sessionId, files, (images) =>
|
|
1121
|
-
|
|
1131
|
+
intakeFiles(sessionId, files, (images) => {
|
|
1132
|
+
void deliverImages(sessionId, images);
|
|
1133
|
+
});
|
|
1134
|
+
},
|
|
1135
|
+
onImages: (files) => {
|
|
1136
|
+
const sessionId = currentSessionId();
|
|
1137
|
+
void deliverImages(sessionId, files);
|
|
1138
|
+
},
|
|
1139
|
+
onText: (text) => pasteAccompanyingText(text)
|
|
1122
1140
|
});
|
|
1123
1141
|
}
|
|
1142
|
+
var INSTANCE = (() => {
|
|
1143
|
+
const w = globalThis;
|
|
1144
|
+
w.__fileNativeInstances = (w.__fileNativeInstances ?? 0) + 1;
|
|
1145
|
+
const n = w.__fileNativeInstances;
|
|
1146
|
+
console.warn(`[file-native] apply #${n}`);
|
|
1147
|
+
return n;
|
|
1148
|
+
})();
|
|
1124
1149
|
function apply(ctx) {
|
|
1125
1150
|
ctx.effect(() => installLiveIntercept(), "file-native: document intercept");
|
|
1126
|
-
|
|
1151
|
+
let sidebar;
|
|
1152
|
+
try {
|
|
1153
|
+
sidebar = ctx.betterSidebar;
|
|
1154
|
+
} catch {
|
|
1155
|
+
sidebar = void 0;
|
|
1156
|
+
}
|
|
1157
|
+
if (sidebar !== void 0 && sidebar.features.includes("openFile")) {
|
|
1158
|
+
chatCardOpener = (sid, relPath, name2) => sidebar.openFile({ sessionId: sid }, relPath, name2);
|
|
1159
|
+
} else {
|
|
1160
|
+
chatCardOpener = (sid, relPath) => {
|
|
1161
|
+
window.open(`/plugins/file-native/file?sessionId=${encodeURIComponent(sid)}&path=${encodeURIComponent(relPath)}`, "_blank");
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
const base = -(1e3 + INSTANCE * 10);
|
|
1165
|
+
const safeInject = (slot, registerFn) => {
|
|
1166
|
+
ctx.slots.inject(slot, () => {
|
|
1167
|
+
try {
|
|
1168
|
+
return registerFn();
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
console.warn(`[file-native] skip ${slot} registration:`, error);
|
|
1171
|
+
return () => {
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
};
|
|
1176
|
+
safeInject("conversation.input.attachments", () => ctx.slots.register({
|
|
1127
1177
|
name: "conversation.input.attachments",
|
|
1128
|
-
priority:
|
|
1178
|
+
priority: base
|
|
1129
1179
|
}, RailSlot));
|
|
1130
|
-
|
|
1180
|
+
safeInject("conversation.input.left", () => ctx.slots.register({
|
|
1131
1181
|
name: "conversation.input.left",
|
|
1132
1182
|
id: "file-native-picker",
|
|
1133
|
-
order: 0
|
|
1183
|
+
order: 0,
|
|
1184
|
+
priority: base
|
|
1134
1185
|
}, 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({
|
|
1141
|
-
name: "conversation.chat.turnTail",
|
|
1142
|
-
priority: -1,
|
|
1143
|
-
select: selectTail
|
|
1144
|
-
}, TailSlot));
|
|
1145
|
-
ctx.logger?.info?.("[file-native] client loaded");
|
|
1186
|
+
console.info(`[file-native] client loaded (instance #${INSTANCE})`);
|
|
1146
1187
|
}
|
|
1147
1188
|
|
|
1148
1189
|
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.1
|
|
4
|
+
"version": "0.2.1",
|
|
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
|
+
}
|