@dai_ming/plugin-deliverables 1.0.19 → 1.0.21
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/INSTALL.md +6 -6
- package/README.md +15 -12
- package/agents-rules/deliverables.md +16 -14
- package/index.js +840 -8
- package/mcp-servers/deliverables.js +130 -32
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -2
- package/skills/deliverables/SKILL.md +7 -0
- package/test/index.test.js +100 -0
- package/test/mcp-server.test.js +44 -0
package/index.js
CHANGED
|
@@ -1,18 +1,87 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
|
|
3
8
|
const FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
|
|
4
9
|
const SUMMARY_CACHE_KEY = "__plugin_deliverables_summary_cache__";
|
|
10
|
+
const PENDING_FILES_KEY = "__plugin_deliverables_pending_files__";
|
|
11
|
+
const UPLOAD_CACHE_KEY = "__plugin_deliverables_upload_cache__";
|
|
5
12
|
const SUMMARY_CACHE_LIMIT = 200;
|
|
6
13
|
const SHORT_SUMMARY_THRESHOLD = 120;
|
|
14
|
+
const DEFAULT_GATEWAY_PUBLIC_URL = "https://claw-gateway.csagentai.com";
|
|
15
|
+
const DEFAULT_GATEWAY_INTERNAL_URL = "http://claw-gateway:8080";
|
|
16
|
+
const AUTO_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
|
17
|
+
const AUTO_UPLOAD_MAX_FILES = 8;
|
|
18
|
+
const AUTO_UPLOAD_MAX_BYTES = 200 * 1024 * 1024;
|
|
19
|
+
const AUTO_UPLOAD_SCAN_MAX_ENTRIES = 4000;
|
|
20
|
+
const AUTO_UPLOAD_SCAN_DEPTH = 5;
|
|
21
|
+
const DELIVERABLE_GATEWAY_PATH_RE = /^\/openclaw-gateway\/(?:output|preview)(?:\/|$)/u;
|
|
22
|
+
const DELIVERABLE_LINK_PREFIX_RE =
|
|
23
|
+
/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]/u;
|
|
24
|
+
const DELIVERABLE_LINK_LABEL_RE =
|
|
25
|
+
/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u;
|
|
26
|
+
const INTERNAL_ABSOLUTE_PATH_RE =
|
|
27
|
+
/(?:\/data\/workspace-[^\s`"'<>,。;:、))\]}]+|\/home\/node\/\.openclaw\/workspace(?:-[A-Za-z0-9_.-]+)?\/[^\s`"'<>,。;:、))\]}]+)/gu;
|
|
28
|
+
const INLINE_FILE_REF_RE = /`([^`\n]{1,512}\.[A-Za-z0-9]{1,16})`/gu;
|
|
29
|
+
const OUTPUT_RELATIVE_REF_RE = /(?:^|[\s(::])((?:\.\/)?output\/[^\s`"'<>,。;:、))\]}]+\.[A-Za-z0-9]{1,16})/gu;
|
|
30
|
+
const FILE_EXTENSION_RE = /\.[A-Za-z0-9]{1,16}$/u;
|
|
31
|
+
const TEXT_EXTENSIONS = new Set([
|
|
32
|
+
".css",
|
|
33
|
+
".csv",
|
|
34
|
+
".html",
|
|
35
|
+
".htm",
|
|
36
|
+
".js",
|
|
37
|
+
".json",
|
|
38
|
+
".jsx",
|
|
39
|
+
".log",
|
|
40
|
+
".md",
|
|
41
|
+
".markdown",
|
|
42
|
+
".mjs",
|
|
43
|
+
".py",
|
|
44
|
+
".sql",
|
|
45
|
+
".svg",
|
|
46
|
+
".ts",
|
|
47
|
+
".tsx",
|
|
48
|
+
".txt",
|
|
49
|
+
".xml",
|
|
50
|
+
".yaml",
|
|
51
|
+
".yml",
|
|
52
|
+
]);
|
|
53
|
+
const IMAGE_EXTENSIONS = new Set([".apng", ".avif", ".bmp", ".gif", ".ico", ".jpeg", ".jpg", ".png", ".svg", ".webp"]);
|
|
54
|
+
const VIDEO_EXTENSIONS = new Set([".avi", ".m4v", ".mkv", ".mov", ".mp4", ".mpeg", ".mpg", ".webm"]);
|
|
55
|
+
const PPT_EXTENSIONS = new Set([".ppt", ".pptx"]);
|
|
56
|
+
const ARCHIVE_EXTENSIONS = new Set([".7z", ".gz", ".rar", ".tar", ".tgz", ".zip"]);
|
|
57
|
+
const IGNORED_PATH_PARTS = new Set([
|
|
58
|
+
".git",
|
|
59
|
+
".hg",
|
|
60
|
+
".svn",
|
|
61
|
+
"node_modules",
|
|
62
|
+
"sessions",
|
|
63
|
+
"skills",
|
|
64
|
+
".cache",
|
|
65
|
+
"__pycache__",
|
|
66
|
+
]);
|
|
67
|
+
const IGNORED_FILE_NAMES = new Set([
|
|
68
|
+
"AGENTS.md",
|
|
69
|
+
"SOUL.md",
|
|
70
|
+
"openclaw.json",
|
|
71
|
+
"openclaw-runtime.json",
|
|
72
|
+
"package-lock.json",
|
|
73
|
+
]);
|
|
7
74
|
|
|
8
75
|
const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
9
76
|
"## Deliverables Runtime Guard (HARD CONSTRAINT)",
|
|
10
77
|
"",
|
|
11
78
|
"- These rules apply to the main agent and all subagents.",
|
|
12
79
|
"- When the user asks for a document, article, report, HTML page, Markdown file, PPT, image, archive, game, or any other file deliverable, you MUST create it under the current workspace `output/` directory and upload it with `deliverables__upload_deliverable`.",
|
|
80
|
+
"- For binary deliverables such as PDF, PPT, images, video, or zip files, pass `file_path` to `deliverables__upload_deliverable` after writing the file under `output/`; do not paste command output or partial base64 into `content_base64`.",
|
|
13
81
|
"- Every single-file deliverable must use a file name with an explicit extension. If the user did not specify a document/text format, default to Markdown and use a `.md` file name.",
|
|
14
82
|
"- Do not use direct message attachments to deliver generated files. This includes the message tool attachment fields such as `media`, `path`, `filePath`, `buffer`, `attachment`, or similar file-carrying fields.",
|
|
15
83
|
"- Do not emit `MEDIA:` file references for user-facing deliverables. Deliverable links must come from the deliverables upload tool instead.",
|
|
84
|
+
"- If the deliverables upload tool returns an error, retry with a valid `file_path` or explain the upload failure; never invent or rewrite OSS/download URLs.",
|
|
16
85
|
"- After a successful upload, reply with a substantive content summary before the links. For documents/articles/reports, prefer 1 short intro plus 3-6 concise bullets covering key sections, highlights, or findings, then preserve the Markdown links returned by the deliverables tool.",
|
|
17
86
|
].join("\n");
|
|
18
87
|
|
|
@@ -20,6 +89,648 @@ function isString(value) {
|
|
|
20
89
|
return typeof value === "string";
|
|
21
90
|
}
|
|
22
91
|
|
|
92
|
+
function trimString(value) {
|
|
93
|
+
return isString(value) ? value.trim() : "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getPendingFileStore() {
|
|
97
|
+
if (!globalThis[PENDING_FILES_KEY] || !(globalThis[PENDING_FILES_KEY] instanceof Map)) {
|
|
98
|
+
globalThis[PENDING_FILES_KEY] = new Map();
|
|
99
|
+
}
|
|
100
|
+
return globalThis[PENDING_FILES_KEY];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getUploadCache() {
|
|
104
|
+
if (!globalThis[UPLOAD_CACHE_KEY] || !(globalThis[UPLOAD_CACHE_KEY] instanceof Map)) {
|
|
105
|
+
globalThis[UPLOAD_CACHE_KEY] = new Map();
|
|
106
|
+
}
|
|
107
|
+
return globalThis[UPLOAD_CACHE_KEY];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function pruneTimedMap(map, ttlMs) {
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
for (const [key, value] of map.entries()) {
|
|
113
|
+
if (!value || !value.createdAt || now - Number(value.createdAt) > ttlMs) {
|
|
114
|
+
map.delete(key);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeSlash(value) {
|
|
120
|
+
return String(value || "").replace(/\\/g, "/");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stripTrailingPathPunctuation(value) {
|
|
124
|
+
return String(value || "").replace(/[.,;:!?,。;:!?、))\]}》】]+$/u, "");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function hasFileExtension(value) {
|
|
128
|
+
return FILE_EXTENSION_RE.test(path.basename(stripTrailingPathPunctuation(value)));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isURLLike(value) {
|
|
132
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(String(value || ""));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fileExtension(filePath) {
|
|
136
|
+
return path.extname(stripTrailingPathPunctuation(filePath)).toLowerCase();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isTextLikeFile(filePath) {
|
|
140
|
+
return TEXT_EXTENSIONS.has(fileExtension(filePath));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function deliverableTypeForPath(filePath, isDirectory) {
|
|
144
|
+
if (isDirectory) {
|
|
145
|
+
return "game";
|
|
146
|
+
}
|
|
147
|
+
const ext = fileExtension(filePath);
|
|
148
|
+
if (IMAGE_EXTENSIONS.has(ext) && ext !== ".svg") {
|
|
149
|
+
return "image";
|
|
150
|
+
}
|
|
151
|
+
if (VIDEO_EXTENSIONS.has(ext)) {
|
|
152
|
+
return "video";
|
|
153
|
+
}
|
|
154
|
+
if (PPT_EXTENSIONS.has(ext)) {
|
|
155
|
+
return "ppt";
|
|
156
|
+
}
|
|
157
|
+
if (ARCHIVE_EXTENSIONS.has(ext)) {
|
|
158
|
+
return "zip";
|
|
159
|
+
}
|
|
160
|
+
return "article";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isAllowedWorkspacePath(candidatePath) {
|
|
164
|
+
const normalized = normalizeSlash(path.resolve(candidatePath));
|
|
165
|
+
return (
|
|
166
|
+
normalized.indexOf("/data/workspace-") === 0 ||
|
|
167
|
+
normalized.indexOf("/home/node/.openclaw/workspace") === 0
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function shouldIgnorePath(candidatePath) {
|
|
172
|
+
const normalized = normalizeSlash(candidatePath);
|
|
173
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
174
|
+
for (const part of parts) {
|
|
175
|
+
if (IGNORED_PATH_PARTS.has(part)) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return IGNORED_FILE_NAMES.has(path.basename(normalized));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function safeStat(candidatePath) {
|
|
183
|
+
try {
|
|
184
|
+
return fs.statSync(candidatePath);
|
|
185
|
+
} catch (_err) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function workspaceRoots() {
|
|
191
|
+
const roots = [];
|
|
192
|
+
["/home/node/.openclaw/workspace-main", "/home/node/.openclaw/workspace"].forEach((root) => {
|
|
193
|
+
if (safeStat(root)) {
|
|
194
|
+
roots.push(root);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
for (const entry of fs.readdirSync("/data")) {
|
|
199
|
+
if (!entry || entry.indexOf("workspace-") !== 0) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const root = path.join("/data", entry);
|
|
203
|
+
if (safeStat(root)) {
|
|
204
|
+
roots.push(root);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (_err) {
|
|
208
|
+
// /data is not guaranteed in non-pod test environments.
|
|
209
|
+
}
|
|
210
|
+
return roots;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveCandidatePath(rawPath) {
|
|
214
|
+
const cleaned = stripTrailingPathPunctuation(trimString(rawPath));
|
|
215
|
+
if (!cleaned || isURLLike(cleaned)) {
|
|
216
|
+
return "";
|
|
217
|
+
}
|
|
218
|
+
if (path.isAbsolute(cleaned)) {
|
|
219
|
+
return path.resolve(cleaned);
|
|
220
|
+
}
|
|
221
|
+
return cleaned;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function findFileByRelativeOrBaseName(rawRef) {
|
|
225
|
+
const cleaned = stripTrailingPathPunctuation(trimString(rawRef)).replace(/^\.\//, "");
|
|
226
|
+
if (!cleaned || isURLLike(cleaned) || !hasFileExtension(cleaned)) {
|
|
227
|
+
return "";
|
|
228
|
+
}
|
|
229
|
+
const roots = workspaceRoots();
|
|
230
|
+
const directCandidates = [];
|
|
231
|
+
for (const root of roots) {
|
|
232
|
+
directCandidates.push(path.join(root, cleaned));
|
|
233
|
+
directCandidates.push(path.join(root, "output", cleaned));
|
|
234
|
+
directCandidates.push(path.join(root, path.basename(cleaned)));
|
|
235
|
+
directCandidates.push(path.join(root, "output", path.basename(cleaned)));
|
|
236
|
+
}
|
|
237
|
+
for (const candidate of directCandidates) {
|
|
238
|
+
const stat = safeStat(candidate);
|
|
239
|
+
if (stat && (stat.isFile() || stat.isDirectory()) && isAllowedWorkspacePath(candidate) && !shouldIgnorePath(candidate)) {
|
|
240
|
+
return path.resolve(candidate);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const basename = path.basename(cleaned);
|
|
245
|
+
const found = [];
|
|
246
|
+
let visited = 0;
|
|
247
|
+
function scan(dir, depth) {
|
|
248
|
+
if (depth < 0 || visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES || shouldIgnorePath(dir)) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
let entries;
|
|
252
|
+
try {
|
|
253
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
254
|
+
} catch (_err) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
for (const entry of entries) {
|
|
258
|
+
if (visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
visited += 1;
|
|
262
|
+
const full = path.join(dir, entry.name);
|
|
263
|
+
if (shouldIgnorePath(full)) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (entry.isDirectory()) {
|
|
267
|
+
scan(full, depth - 1);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (entry.isFile() && entry.name === basename) {
|
|
271
|
+
const stat = safeStat(full);
|
|
272
|
+
if (stat) {
|
|
273
|
+
found.push({ path: full, mtimeMs: stat.mtimeMs });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const root of roots) {
|
|
279
|
+
scan(root, AUTO_UPLOAD_SCAN_DEPTH);
|
|
280
|
+
}
|
|
281
|
+
found.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
282
|
+
return found.length > 0 ? path.resolve(found[0].path) : "";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function extractFileReferencesFromText(text) {
|
|
286
|
+
const normalized = String(text || "");
|
|
287
|
+
const refs = [];
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
function add(raw, value, kind) {
|
|
290
|
+
const cleaned = stripTrailingPathPunctuation(value);
|
|
291
|
+
if (!cleaned || isURLLike(cleaned)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const key = `${kind}:${cleaned}`;
|
|
295
|
+
if (seen.has(key)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
seen.add(key);
|
|
299
|
+
refs.push({ raw, value: cleaned, kind });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const match of normalized.matchAll(INTERNAL_ABSOLUTE_PATH_RE)) {
|
|
303
|
+
add(match[0], match[0], "absolute");
|
|
304
|
+
}
|
|
305
|
+
for (const match of normalized.matchAll(INLINE_FILE_REF_RE)) {
|
|
306
|
+
add(match[0], match[1], "inline");
|
|
307
|
+
}
|
|
308
|
+
for (const match of normalized.matchAll(OUTPUT_RELATIVE_REF_RE)) {
|
|
309
|
+
add(match[1], match[1], "relative");
|
|
310
|
+
}
|
|
311
|
+
return refs;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function resolveFileReference(ref) {
|
|
315
|
+
if (!ref || !ref.value) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const candidate = resolveCandidatePath(ref.value);
|
|
319
|
+
const resolved = path.isAbsolute(candidate) ? candidate : findFileByRelativeOrBaseName(candidate);
|
|
320
|
+
if (!resolved) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const stat = safeStat(resolved);
|
|
324
|
+
if (!stat || (!stat.isFile() && !stat.isDirectory())) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (!isAllowedWorkspacePath(resolved) || shouldIgnorePath(resolved)) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
raw: ref.raw,
|
|
332
|
+
path: resolved,
|
|
333
|
+
fileName: path.basename(resolved),
|
|
334
|
+
isDirectory: stat.isDirectory(),
|
|
335
|
+
size: stat.isDirectory() ? 0 : stat.size,
|
|
336
|
+
mtimeMs: stat.mtimeMs,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function collectDirectoryFiles(dirPath) {
|
|
341
|
+
const files = [];
|
|
342
|
+
let total = 0;
|
|
343
|
+
let visited = 0;
|
|
344
|
+
function scan(current, depth) {
|
|
345
|
+
if (depth < 0 || visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES || shouldIgnorePath(current)) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let entries;
|
|
349
|
+
try {
|
|
350
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
351
|
+
} catch (_err) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
if (visited > AUTO_UPLOAD_SCAN_MAX_ENTRIES || files.length >= AUTO_UPLOAD_MAX_FILES * 50) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
visited += 1;
|
|
359
|
+
const full = path.join(current, entry.name);
|
|
360
|
+
if (shouldIgnorePath(full)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (entry.isDirectory()) {
|
|
364
|
+
scan(full, depth - 1);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (!entry.isFile()) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const stat = safeStat(full);
|
|
371
|
+
if (!stat || total + stat.size > AUTO_UPLOAD_MAX_BYTES) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
total += stat.size;
|
|
375
|
+
const rel = normalizeSlash(path.relative(dirPath, full));
|
|
376
|
+
if (isTextLikeFile(full)) {
|
|
377
|
+
files.push({ name: rel, contentText: fs.readFileSync(full, "utf8") });
|
|
378
|
+
} else {
|
|
379
|
+
files.push({ name: rel, contentBase64: fs.readFileSync(full).toString("base64") });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
scan(dirPath, AUTO_UPLOAD_SCAN_DEPTH);
|
|
384
|
+
return files;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function extractStringField(obj, keys) {
|
|
388
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
389
|
+
return "";
|
|
390
|
+
}
|
|
391
|
+
for (const key of keys) {
|
|
392
|
+
const value = obj[key];
|
|
393
|
+
if (isString(value) && value.trim()) {
|
|
394
|
+
return value.trim();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return "";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function extractContextString(event, keys) {
|
|
401
|
+
const direct = extractStringField(event, keys);
|
|
402
|
+
if (direct) {
|
|
403
|
+
return direct;
|
|
404
|
+
}
|
|
405
|
+
const containers = [
|
|
406
|
+
event && event.params,
|
|
407
|
+
event && event.metadata,
|
|
408
|
+
event && event.context,
|
|
409
|
+
event && event.ctx,
|
|
410
|
+
event && event.message,
|
|
411
|
+
];
|
|
412
|
+
for (const container of containers) {
|
|
413
|
+
const value = extractStringField(container, keys);
|
|
414
|
+
if (value) {
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return "";
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function payloadResourceId(body) {
|
|
422
|
+
return extractStringField(body, ["resource_id", "resourceId", "conversation_id", "conversationId"]);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function payloadGroupId(body) {
|
|
426
|
+
return extractStringField(body, ["group_id", "groupId", "conversation_id", "conversationId"]);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function payloadUserId(body) {
|
|
430
|
+
return extractStringField(body, ["user_id", "userId", "sender_id", "senderId", "owner_id", "ownerId"]);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function isWriteTool(toolName) {
|
|
434
|
+
const normalized = normalizeToolName(toolName);
|
|
435
|
+
return normalized === "write" || normalized.endsWith("__write") || normalized === "write_file" || normalized.endsWith("__write_file");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function extractWritePath(params) {
|
|
439
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
return extractStringField(params, [
|
|
443
|
+
"path",
|
|
444
|
+
"file_path",
|
|
445
|
+
"filePath",
|
|
446
|
+
"filename",
|
|
447
|
+
"fileName",
|
|
448
|
+
"target",
|
|
449
|
+
"target_path",
|
|
450
|
+
"targetPath",
|
|
451
|
+
"destination",
|
|
452
|
+
]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function rememberPendingFile(event) {
|
|
456
|
+
const params = event && event.params;
|
|
457
|
+
const writePath = extractWritePath(params);
|
|
458
|
+
if (!writePath || isURLLike(writePath)) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const store = getPendingFileStore();
|
|
462
|
+
pruneTimedMap(store, AUTO_UPLOAD_TTL_MS);
|
|
463
|
+
const resourceId = extractContextString(event, ["resource_id", "resourceId", "conversation_id", "conversationId"]);
|
|
464
|
+
const key = `${resourceId || "unknown"}:${writePath}`;
|
|
465
|
+
store.set(key, {
|
|
466
|
+
path: writePath,
|
|
467
|
+
resourceId,
|
|
468
|
+
createdAt: Date.now(),
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function pendingCandidatesForPayload(body) {
|
|
473
|
+
const resourceId = payloadResourceId(body);
|
|
474
|
+
if (!resourceId) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
const store = getPendingFileStore();
|
|
478
|
+
pruneTimedMap(store, AUTO_UPLOAD_TTL_MS);
|
|
479
|
+
const out = [];
|
|
480
|
+
for (const entry of store.values()) {
|
|
481
|
+
if (!entry || entry.resourceId !== resourceId) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const ref = { raw: entry.path, value: entry.path, kind: "pending" };
|
|
485
|
+
const resolved = resolveFileReference(ref);
|
|
486
|
+
if (resolved) {
|
|
487
|
+
out.push(resolved);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function gatewayURL() {
|
|
494
|
+
return (process.env.CLAW_GATEWAY_URL || DEFAULT_GATEWAY_INTERNAL_URL).replace(/\/$/, "");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function gatewayPublicURL() {
|
|
498
|
+
return (process.env.CLAW_GATEWAY_PUBLIC_URL || process.env.CLAW_GATEWAY_URL || DEFAULT_GATEWAY_PUBLIC_URL).replace(/\/$/, "");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function apiKey() {
|
|
502
|
+
return process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY || "api-key-1";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function deriveReleaseName() {
|
|
506
|
+
let release = process.env.botID || process.env.OPENCLAW_RELEASE || process.env.BOT_ID || "";
|
|
507
|
+
if (!release) {
|
|
508
|
+
const tok = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
509
|
+
if (tok.indexOf("oc-") === 0) {
|
|
510
|
+
release = tok.slice(3);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return release;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function httpJSONRequest(method, requestPath, body) {
|
|
517
|
+
return new Promise((resolve, reject) => {
|
|
518
|
+
let parsed;
|
|
519
|
+
try {
|
|
520
|
+
parsed = new URL(gatewayURL() + requestPath);
|
|
521
|
+
} catch (err) {
|
|
522
|
+
reject(err);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const isHTTPS = parsed.protocol === "https:";
|
|
526
|
+
const transport = isHTTPS ? https : http;
|
|
527
|
+
const bodyStr = body ? JSON.stringify(body) : "";
|
|
528
|
+
const req = transport.request(
|
|
529
|
+
{
|
|
530
|
+
hostname: parsed.hostname,
|
|
531
|
+
port: parsed.port || (isHTTPS ? 443 : 80),
|
|
532
|
+
path: parsed.pathname + (parsed.search || ""),
|
|
533
|
+
method,
|
|
534
|
+
headers: {
|
|
535
|
+
"Content-Type": "application/json",
|
|
536
|
+
"X-API-Key": apiKey(),
|
|
537
|
+
"Content-Length": Buffer.byteLength(bodyStr),
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
(res) => {
|
|
541
|
+
const chunks = [];
|
|
542
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
543
|
+
res.on("end", () => {
|
|
544
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
545
|
+
let obj;
|
|
546
|
+
try {
|
|
547
|
+
obj = JSON.parse(text);
|
|
548
|
+
} catch (_err) {
|
|
549
|
+
reject(new Error(`gateway ${res.statusCode}: non-JSON response: ${text.slice(0, 200)}`));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (res.statusCode >= 400) {
|
|
553
|
+
reject(new Error(`gateway ${res.statusCode}: ${obj.message || text.slice(0, 200)}`));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
resolve(obj);
|
|
557
|
+
});
|
|
558
|
+
},
|
|
559
|
+
);
|
|
560
|
+
req.on("error", reject);
|
|
561
|
+
if (bodyStr) {
|
|
562
|
+
req.write(bodyStr);
|
|
563
|
+
}
|
|
564
|
+
req.end();
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function uploadCacheKey(candidate, resourceId) {
|
|
569
|
+
return `${resourceId || ""}:${candidate.path}:${candidate.mtimeMs || 0}:${candidate.size || 0}`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function uploadCandidate(candidate, body) {
|
|
573
|
+
const resourceId = payloadResourceId(body);
|
|
574
|
+
const cache = getUploadCache();
|
|
575
|
+
pruneTimedMap(cache, AUTO_UPLOAD_TTL_MS);
|
|
576
|
+
const cacheKey = uploadCacheKey(candidate, resourceId);
|
|
577
|
+
const cached = cache.get(cacheKey);
|
|
578
|
+
if (cached && cached.result) {
|
|
579
|
+
return cached.result;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const stat = safeStat(candidate.path);
|
|
583
|
+
if (!stat) {
|
|
584
|
+
throw new Error(`file no longer exists: ${candidate.fileName}`);
|
|
585
|
+
}
|
|
586
|
+
const requestBody = {
|
|
587
|
+
resourceId,
|
|
588
|
+
groupId: payloadGroupId(body),
|
|
589
|
+
userId: payloadUserId(body),
|
|
590
|
+
release: deriveReleaseName(),
|
|
591
|
+
type: deliverableTypeForPath(candidate.path, stat.isDirectory()),
|
|
592
|
+
fileName: candidate.fileName || path.basename(candidate.path),
|
|
593
|
+
};
|
|
594
|
+
if (stat.isDirectory()) {
|
|
595
|
+
const files = collectDirectoryFiles(candidate.path);
|
|
596
|
+
if (files.length === 0) {
|
|
597
|
+
throw new Error(`directory has no uploadable files: ${candidate.fileName}`);
|
|
598
|
+
}
|
|
599
|
+
requestBody.files = files;
|
|
600
|
+
} else {
|
|
601
|
+
if (stat.size > AUTO_UPLOAD_MAX_BYTES) {
|
|
602
|
+
throw new Error(`file exceeds auto-upload limit: ${candidate.fileName}`);
|
|
603
|
+
}
|
|
604
|
+
requestBody.contentBase64 = fs.readFileSync(candidate.path).toString("base64");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const response = await httpJSONRequest("POST", "/openclaw-gateway/be/deliverables", requestBody);
|
|
608
|
+
const data = response.data || response;
|
|
609
|
+
const previewURL = data.previewUrl || `${gatewayPublicURL()}/openclaw-gateway/preview/${data.uuid}`;
|
|
610
|
+
const result = {
|
|
611
|
+
fileName: requestBody.fileName,
|
|
612
|
+
uuid: data.uuid,
|
|
613
|
+
previewURL,
|
|
614
|
+
downloadURL: data.downloadUrl || previewURL,
|
|
615
|
+
};
|
|
616
|
+
cache.set(cacheKey, { result, createdAt: Date.now() });
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function dedupeCandidates(candidates) {
|
|
621
|
+
const out = [];
|
|
622
|
+
const seen = new Set();
|
|
623
|
+
for (const candidate of candidates) {
|
|
624
|
+
if (!candidate || !candidate.path) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const key = path.resolve(candidate.path);
|
|
628
|
+
if (seen.has(key)) {
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
seen.add(key);
|
|
632
|
+
out.push(candidate);
|
|
633
|
+
if (out.length >= AUTO_UPLOAD_MAX_FILES) {
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function escapeMarkdownLabel(label) {
|
|
641
|
+
return String(label || "文件").replace(/[[\]()`]/g, "");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function removeInternalPathLeaks(text, candidates) {
|
|
645
|
+
let out = String(text || "");
|
|
646
|
+
for (const candidate of candidates) {
|
|
647
|
+
if (!candidate || !candidate.raw) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
out = out.split(candidate.raw).join(candidate.fileName || path.basename(candidate.path));
|
|
651
|
+
}
|
|
652
|
+
return out;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function appendDeliverableLinks(text, uploads) {
|
|
656
|
+
const lines = [];
|
|
657
|
+
uploads.forEach((upload) => {
|
|
658
|
+
const label = escapeMarkdownLabel(upload.fileName);
|
|
659
|
+
if (upload.previewURL) {
|
|
660
|
+
lines.push(`预览链接:[${label}](${upload.previewURL})`);
|
|
661
|
+
}
|
|
662
|
+
if (upload.downloadURL && upload.downloadURL !== upload.previewURL) {
|
|
663
|
+
lines.push(`下载链接:[${label}](${upload.downloadURL})`);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
if (lines.length === 0) {
|
|
667
|
+
return text;
|
|
668
|
+
}
|
|
669
|
+
return `${String(text || "").trim()}\n\n交付物链接:\n${lines.join("\n")}`;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function hasTrustedDeliverableLink(text) {
|
|
673
|
+
return splitDeliverableMessage(text) !== null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function shouldSkipAutoUploadPayload(body) {
|
|
677
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
if (!isString(body.content) || !body.content.trim()) {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
if (body.stream_id || body.seq !== undefined || body.delta !== undefined || body.is_final !== undefined) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
if (body.palz_msg_type || body.tool_content) {
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function autoUploadAndRewritePayload(body, api) {
|
|
693
|
+
if (shouldSkipAutoUploadPayload(body) || hasTrustedDeliverableLink(body.content)) {
|
|
694
|
+
return body;
|
|
695
|
+
}
|
|
696
|
+
const refs = extractFileReferencesFromText(body.content);
|
|
697
|
+
const explicitCandidates = refs.map(resolveFileReference).filter(Boolean);
|
|
698
|
+
const candidates = dedupeCandidates(explicitCandidates.concat(pendingCandidatesForPayload(body)));
|
|
699
|
+
if (candidates.length === 0) {
|
|
700
|
+
return body;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const uploads = [];
|
|
704
|
+
const failures = [];
|
|
705
|
+
for (const candidate of candidates) {
|
|
706
|
+
try {
|
|
707
|
+
uploads.push(await uploadCandidate(candidate, body));
|
|
708
|
+
} catch (err) {
|
|
709
|
+
failures.push({ candidate, error: err });
|
|
710
|
+
api.logger.warn?.(
|
|
711
|
+
`[plugin-deliverables] auto-upload failed file=${candidate.fileName || candidate.path} error=${err.message}`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (uploads.length === 0) {
|
|
716
|
+
if (explicitCandidates.length === 0) {
|
|
717
|
+
return body;
|
|
718
|
+
}
|
|
719
|
+
const next = Object.assign({}, body);
|
|
720
|
+
next.content = "交付物上传失败,已阻止发送工作区内部文件路径。请稍后重试或联系管理员排查上传链路。";
|
|
721
|
+
return next;
|
|
722
|
+
}
|
|
723
|
+
const next = Object.assign({}, body);
|
|
724
|
+
next.content = appendDeliverableLinks(removeInternalPathLeaks(body.content, candidates), uploads);
|
|
725
|
+
if (failures.length > 0) {
|
|
726
|
+
next.content += `\n\n另有 ${failures.length} 个文件上传失败,已避免发送内部路径。`;
|
|
727
|
+
}
|
|
728
|
+
api.logger.info?.(
|
|
729
|
+
`[plugin-deliverables] auto-uploaded ${uploads.length} workspace file(s) for target=${String(body.conversation_id || "")}`,
|
|
730
|
+
);
|
|
731
|
+
return next;
|
|
732
|
+
}
|
|
733
|
+
|
|
23
734
|
function getSummaryCache() {
|
|
24
735
|
if (!globalThis[SUMMARY_CACHE_KEY] || !(globalThis[SUMMARY_CACHE_KEY] instanceof Map)) {
|
|
25
736
|
globalThis[SUMMARY_CACHE_KEY] = new Map();
|
|
@@ -392,16 +1103,97 @@ function extractDeliverableURL(line) {
|
|
|
392
1103
|
return "";
|
|
393
1104
|
}
|
|
394
1105
|
|
|
1106
|
+
function configuredGatewayHosts() {
|
|
1107
|
+
const values = [
|
|
1108
|
+
process.env.CLAW_GATEWAY_PUBLIC_URL,
|
|
1109
|
+
process.env.CLAW_GATEWAY_URL,
|
|
1110
|
+
DEFAULT_GATEWAY_PUBLIC_URL,
|
|
1111
|
+
DEFAULT_GATEWAY_INTERNAL_URL,
|
|
1112
|
+
];
|
|
1113
|
+
const hosts = new Set();
|
|
1114
|
+
for (const value of values) {
|
|
1115
|
+
if (!isString(value) || !value.trim()) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
const parsed = new URL(value.trim());
|
|
1120
|
+
hosts.add(parsed.host.toLowerCase());
|
|
1121
|
+
hosts.add(parsed.hostname.toLowerCase());
|
|
1122
|
+
} catch (_err) {
|
|
1123
|
+
// Ignore malformed configuration placeholders.
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return hosts;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function isTrustedDeliverableURL(rawURL) {
|
|
1130
|
+
if (!isString(rawURL) || !rawURL.trim()) {
|
|
1131
|
+
return false;
|
|
1132
|
+
}
|
|
1133
|
+
let parsed;
|
|
1134
|
+
try {
|
|
1135
|
+
parsed = new URL(rawURL.trim());
|
|
1136
|
+
} catch (_err) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
if (!DELIVERABLE_GATEWAY_PATH_RE.test(parsed.pathname)) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
const hosts = configuredGatewayHosts();
|
|
1143
|
+
const host = parsed.host.toLowerCase();
|
|
1144
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1145
|
+
return hosts.has(host) || hosts.has(hostname);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function isOSSLikeURL(rawURL) {
|
|
1149
|
+
if (!isString(rawURL) || !rawURL.trim()) {
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
let parsed;
|
|
1153
|
+
try {
|
|
1154
|
+
parsed = new URL(rawURL.trim());
|
|
1155
|
+
} catch (_err) {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
const host = parsed.hostname.toLowerCase();
|
|
1159
|
+
return (
|
|
1160
|
+
host.includes("aliyuncs.com") ||
|
|
1161
|
+
host.includes("oss-cn-") ||
|
|
1162
|
+
parsed.searchParams.has("OSSAccessKeyId")
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function findUntrustedOSSDeliverableURL(text) {
|
|
1167
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n");
|
|
1168
|
+
if (!normalized.trim()) {
|
|
1169
|
+
return "";
|
|
1170
|
+
}
|
|
1171
|
+
const lines = normalized.split("\n");
|
|
1172
|
+
let hasTrustedDeliverableURL = false;
|
|
1173
|
+
let firstSuspiciousURL = "";
|
|
1174
|
+
for (const line of lines) {
|
|
1175
|
+
const url = extractDeliverableURL(line);
|
|
1176
|
+
if (!url) {
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (isTrustedDeliverableURL(url)) {
|
|
1180
|
+
hasTrustedDeliverableURL = true;
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (!firstSuspiciousURL && DELIVERABLE_LINK_PREFIX_RE.test(line) && isOSSLikeURL(url)) {
|
|
1184
|
+
firstSuspiciousURL = url;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return !hasTrustedDeliverableURL ? firstSuspiciousURL : "";
|
|
1188
|
+
}
|
|
1189
|
+
|
|
395
1190
|
function isDeliverableLinkLine(line) {
|
|
396
1191
|
const text = String(line || "");
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
)
|
|
401
|
-
) {
|
|
402
|
-
return true;
|
|
1192
|
+
const url = extractDeliverableURL(text);
|
|
1193
|
+
if (!url || !isTrustedDeliverableURL(url)) {
|
|
1194
|
+
return false;
|
|
403
1195
|
}
|
|
404
|
-
return
|
|
1196
|
+
return DELIVERABLE_LINK_LABEL_RE.test(text) || !!url;
|
|
405
1197
|
}
|
|
406
1198
|
|
|
407
1199
|
function splitDeliverableMessage(text) {
|
|
@@ -456,6 +1248,7 @@ function splitDeliverableMessage(text) {
|
|
|
456
1248
|
return {
|
|
457
1249
|
summary,
|
|
458
1250
|
links,
|
|
1251
|
+
linkUrls,
|
|
459
1252
|
primaryLinkUrl: linkUrls.length > 0 ? linkUrls[0] : "",
|
|
460
1253
|
};
|
|
461
1254
|
}
|
|
@@ -478,6 +1271,14 @@ function cloneBodyAsFileLink(body, fileUrl, suffix) {
|
|
|
478
1271
|
return next;
|
|
479
1272
|
}
|
|
480
1273
|
|
|
1274
|
+
function cloneBodyAsBlockedDeliverableLink(body) {
|
|
1275
|
+
return cloneBody(
|
|
1276
|
+
body,
|
|
1277
|
+
"交付物上传未成功,已阻止发送未通过 gateway 交付物系统生成的 OSS 文件链接。请重新通过交付物上传工具上传后再发送。",
|
|
1278
|
+
"",
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
481
1282
|
function shouldSplitPalzPayload(body) {
|
|
482
1283
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
483
1284
|
return null;
|
|
@@ -580,13 +1381,29 @@ function installPalzFetchPatch(api) {
|
|
|
580
1381
|
return originalFetch(input, init);
|
|
581
1382
|
}
|
|
582
1383
|
|
|
1384
|
+
const rewritten = await autoUploadAndRewritePayload(parsed, api);
|
|
1385
|
+
if (rewritten !== parsed) {
|
|
1386
|
+
parsed = rewritten;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
583
1389
|
const split = shouldSplitPalzPayload(parsed);
|
|
584
1390
|
if (!split) {
|
|
1391
|
+
const suspiciousURL = isString(parsed.content)
|
|
1392
|
+
? findUntrustedOSSDeliverableURL(parsed.content)
|
|
1393
|
+
: "";
|
|
1394
|
+
if (suspiciousURL) {
|
|
1395
|
+
const blockedBody = cloneBodyAsBlockedDeliverableLink(parsed);
|
|
1396
|
+
const blockedBodyStr = JSON.stringify(blockedBody);
|
|
1397
|
+
api.logger.warn?.(
|
|
1398
|
+
`[plugin-deliverables] blocked untrusted OSS deliverable link target=${String(parsed.conversation_id || "")} url=${suspiciousURL}`,
|
|
1399
|
+
);
|
|
1400
|
+
return originalFetch(input, Object.assign({}, init, { body: blockedBodyStr }));
|
|
1401
|
+
}
|
|
585
1402
|
return originalFetch(input, init);
|
|
586
1403
|
}
|
|
587
1404
|
|
|
588
1405
|
const summaryBody = cloneBody(parsed, split.summary, "__summary");
|
|
589
|
-
const linksBody = split.primaryLinkUrl
|
|
1406
|
+
const linksBody = split.primaryLinkUrl && (!Array.isArray(split.linkUrls) || split.linkUrls.length === 1)
|
|
590
1407
|
? cloneBodyAsFileLink(parsed, split.primaryLinkUrl, "__links")
|
|
591
1408
|
: cloneBody(parsed, split.links, "__links");
|
|
592
1409
|
const summaryBodyStr = JSON.stringify(summaryBody);
|
|
@@ -650,6 +1467,10 @@ const plugin = {
|
|
|
650
1467
|
cacheUploadSummary(event.params);
|
|
651
1468
|
return;
|
|
652
1469
|
}
|
|
1470
|
+
if (isWriteTool(event.toolName)) {
|
|
1471
|
+
rememberPendingFile(event);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
653
1474
|
if (!isOutboundMessageTool(event.toolName)) {
|
|
654
1475
|
return;
|
|
655
1476
|
}
|
|
@@ -679,5 +1500,16 @@ const plugin = {
|
|
|
679
1500
|
},
|
|
680
1501
|
};
|
|
681
1502
|
|
|
1503
|
+
plugin.__test = {
|
|
1504
|
+
deliverableTypeForPath,
|
|
1505
|
+
extractFileReferencesFromText,
|
|
1506
|
+
extractDeliverableURL,
|
|
1507
|
+
findUntrustedOSSDeliverableURL,
|
|
1508
|
+
isDeliverableLinkLine,
|
|
1509
|
+
isOSSLikeURL,
|
|
1510
|
+
isTrustedDeliverableURL,
|
|
1511
|
+
splitDeliverableMessage,
|
|
1512
|
+
};
|
|
1513
|
+
|
|
682
1514
|
module.exports = plugin;
|
|
683
1515
|
module.exports.default = plugin;
|