@dai_ming/plugin-deliverables 1.0.14 → 1.0.16
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 +216 -0
- package/README.md +188 -90
- package/agents-rules/deliverables.md +30 -34
- package/index.js +239 -55
- package/mcp-servers/deliverables.js +43 -324
- package/openclaw-plugin.json +2 -4
- package/openclaw.plugin.json +4 -3
- package/package.json +9 -9
- package/skills/deliverables/SKILL.md +12 -20
- package/install.js +0 -559
package/index.js
CHANGED
|
@@ -1,14 +1,116 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
const FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
|
|
4
|
+
|
|
5
|
+
const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
6
|
+
"## Deliverables Runtime Guard (HARD CONSTRAINT)",
|
|
7
|
+
"",
|
|
8
|
+
"- These rules apply to the main agent and all subagents.",
|
|
9
|
+
"- 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`.",
|
|
10
|
+
"- 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.",
|
|
11
|
+
"- Do not emit `MEDIA:` file references for user-facing deliverables. Deliverable links must come from the deliverables upload tool instead.",
|
|
12
|
+
"- After a successful upload, reply with a short content-aware intro and then preserve the Markdown links returned by the deliverables tool.",
|
|
13
|
+
].join("\n");
|
|
5
14
|
|
|
6
15
|
function isString(value) {
|
|
7
16
|
return typeof value === "string";
|
|
8
17
|
}
|
|
9
18
|
|
|
19
|
+
const ATTACHMENT_PARAM_KEYS = new Set([
|
|
20
|
+
"attachment",
|
|
21
|
+
"attachments",
|
|
22
|
+
"buffer",
|
|
23
|
+
"file",
|
|
24
|
+
"filepath",
|
|
25
|
+
"files",
|
|
26
|
+
"filename",
|
|
27
|
+
"media",
|
|
28
|
+
"mediaurl",
|
|
29
|
+
"mediaurls",
|
|
30
|
+
"path",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function normalizeToolName(toolName) {
|
|
34
|
+
return typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isDeliverablesUploadTool(toolName) {
|
|
38
|
+
const normalized = normalizeToolName(toolName);
|
|
39
|
+
return normalized === "upload_deliverable" || normalized.endsWith("__upload_deliverable");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isOutboundMessageTool(toolName) {
|
|
43
|
+
const normalized = normalizeToolName(toolName);
|
|
44
|
+
if (!normalized) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return (
|
|
48
|
+
normalized === "message" ||
|
|
49
|
+
normalized.endsWith("__message") ||
|
|
50
|
+
normalized.includes("sendattachment") ||
|
|
51
|
+
normalized.includes("send_attachment")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasNonEmptyValue(value) {
|
|
56
|
+
if (typeof value === "string") {
|
|
57
|
+
return value.trim().length > 0;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return value.length > 0;
|
|
61
|
+
}
|
|
62
|
+
if (value && typeof value === "object") {
|
|
63
|
+
return Object.keys(value).length > 0;
|
|
64
|
+
}
|
|
65
|
+
return Boolean(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function containsMediaToken(value) {
|
|
69
|
+
if (typeof value === "string") {
|
|
70
|
+
return /(^|\n)\s*MEDIA:\s*\S+/i.test(value);
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return value.some(containsMediaToken);
|
|
74
|
+
}
|
|
75
|
+
if (!value || typeof value !== "object") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return Object.values(value).some(containsMediaToken);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isDirectAttachmentBypass(params) {
|
|
82
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
for (const [key, value] of Object.entries(params)) {
|
|
86
|
+
if (ATTACHMENT_PARAM_KEYS.has(String(key).trim().toLowerCase()) && hasNonEmptyValue(value)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return containsMediaToken(params);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractMediaUrls(metadata) {
|
|
94
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const urls = [];
|
|
98
|
+
const mediaUrls = metadata.mediaUrls;
|
|
99
|
+
if (Array.isArray(mediaUrls)) {
|
|
100
|
+
for (const entry of mediaUrls) {
|
|
101
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
102
|
+
urls.push(entry.trim());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (typeof metadata.mediaUrl === "string" && metadata.mediaUrl.trim()) {
|
|
107
|
+
urls.push(metadata.mediaUrl.trim());
|
|
108
|
+
}
|
|
109
|
+
return urls;
|
|
110
|
+
}
|
|
111
|
+
|
|
10
112
|
function trimTrailingBlankLines(lines) {
|
|
11
|
-
|
|
113
|
+
const out = lines.slice();
|
|
12
114
|
while (out.length > 0 && /^\s*$/.test(out[out.length - 1])) {
|
|
13
115
|
out.pop();
|
|
14
116
|
}
|
|
@@ -24,15 +126,15 @@ function isLinksHeading(line) {
|
|
|
24
126
|
}
|
|
25
127
|
|
|
26
128
|
function extractDeliverableURL(line) {
|
|
27
|
-
|
|
129
|
+
const text = String(line || "").trim();
|
|
28
130
|
if (!text) {
|
|
29
131
|
return "";
|
|
30
132
|
}
|
|
31
|
-
|
|
133
|
+
const markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
|
|
32
134
|
if (markdownLink && markdownLink[1]) {
|
|
33
135
|
return markdownLink[1].trim();
|
|
34
136
|
}
|
|
35
|
-
|
|
137
|
+
const rawLink = text.match(/https?:\/\/\S+/iu);
|
|
36
138
|
if (rawLink && rawLink[0]) {
|
|
37
139
|
return rawLink[0].trim();
|
|
38
140
|
}
|
|
@@ -40,23 +142,26 @@ function extractDeliverableURL(line) {
|
|
|
40
142
|
}
|
|
41
143
|
|
|
42
144
|
function isDeliverableLinkLine(line) {
|
|
43
|
-
|
|
44
|
-
if (
|
|
145
|
+
const text = String(line || "");
|
|
146
|
+
if (
|
|
147
|
+
/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(
|
|
148
|
+
text,
|
|
149
|
+
)
|
|
150
|
+
) {
|
|
45
151
|
return true;
|
|
46
152
|
}
|
|
47
153
|
return !!extractDeliverableURL(text);
|
|
48
154
|
}
|
|
49
155
|
|
|
50
156
|
function splitDeliverableMessage(text) {
|
|
51
|
-
|
|
157
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n").trim();
|
|
52
158
|
if (!normalized) {
|
|
53
159
|
return null;
|
|
54
160
|
}
|
|
55
161
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
for (i = 0; i < lines.length; i += 1) {
|
|
162
|
+
const lines = normalized.split("\n");
|
|
163
|
+
let firstLinkIndex = -1;
|
|
164
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
60
165
|
if (isDeliverableLinkLine(lines[i])) {
|
|
61
166
|
firstLinkIndex = i;
|
|
62
167
|
break;
|
|
@@ -66,41 +171,38 @@ function splitDeliverableMessage(text) {
|
|
|
66
171
|
return null;
|
|
67
172
|
}
|
|
68
173
|
|
|
69
|
-
|
|
174
|
+
let summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
|
|
70
175
|
if (summaryLines.length > 0 && isLinksHeading(summaryLines[summaryLines.length - 1])) {
|
|
71
176
|
summaryLines.pop();
|
|
72
177
|
}
|
|
73
178
|
summaryLines = trimTrailingBlankLines(summaryLines);
|
|
74
179
|
|
|
75
|
-
|
|
76
|
-
for (i = firstLinkIndex; i < lines.length; i += 1) {
|
|
77
|
-
|
|
180
|
+
const linkLines = [];
|
|
181
|
+
for (let i = firstLinkIndex; i < lines.length; i += 1) {
|
|
182
|
+
const line = lines[i];
|
|
78
183
|
if (isLinksHeading(line)) {
|
|
79
184
|
continue;
|
|
80
185
|
}
|
|
81
186
|
if (isDeliverableLinkLine(line)) {
|
|
82
|
-
|
|
187
|
+
const url = extractDeliverableURL(stripBulletPrefix(line));
|
|
83
188
|
if (url) {
|
|
84
189
|
linkLines.push(url);
|
|
85
190
|
}
|
|
86
191
|
}
|
|
87
192
|
}
|
|
88
193
|
|
|
89
|
-
|
|
90
|
-
|
|
194
|
+
const summary = summaryLines.join("\n").trim();
|
|
195
|
+
const links = linkLines.length > 0 ? linkLines[0] : "";
|
|
91
196
|
if (!summary || !links) {
|
|
92
197
|
return null;
|
|
93
198
|
}
|
|
94
199
|
|
|
95
|
-
return {
|
|
96
|
-
summary: summary,
|
|
97
|
-
links: links,
|
|
98
|
-
};
|
|
200
|
+
return { summary, links };
|
|
99
201
|
}
|
|
100
202
|
|
|
101
203
|
function cloneBody(body, content, suffix) {
|
|
102
|
-
|
|
103
|
-
Object.keys(body).forEach(
|
|
204
|
+
const next = {};
|
|
205
|
+
Object.keys(body).forEach((key) => {
|
|
104
206
|
next[key] = body[key];
|
|
105
207
|
});
|
|
106
208
|
next.content = content;
|
|
@@ -117,7 +219,12 @@ function shouldSplitPalzPayload(body) {
|
|
|
117
219
|
if (!isString(body.content)) {
|
|
118
220
|
return null;
|
|
119
221
|
}
|
|
120
|
-
if (
|
|
222
|
+
if (
|
|
223
|
+
body.stream_id ||
|
|
224
|
+
body.seq !== undefined ||
|
|
225
|
+
body.delta !== undefined ||
|
|
226
|
+
body.is_final !== undefined
|
|
227
|
+
) {
|
|
121
228
|
return null;
|
|
122
229
|
}
|
|
123
230
|
if (body.palz_msg_type || body.tool_content) {
|
|
@@ -146,72 +253,149 @@ function resolveFetchMethod(input, init) {
|
|
|
146
253
|
return "GET";
|
|
147
254
|
}
|
|
148
255
|
|
|
256
|
+
function previewText(text, limit = 1000) {
|
|
257
|
+
const normalized = String(text || "");
|
|
258
|
+
if (normalized.length <= limit) {
|
|
259
|
+
return normalized;
|
|
260
|
+
}
|
|
261
|
+
return `${normalized.slice(0, limit)}...<truncated>`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function readResponseText(response) {
|
|
265
|
+
if (!response || typeof response.clone !== "function") {
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
return await response.clone().text();
|
|
270
|
+
} catch (_err) {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
149
275
|
function installPalzFetchPatch(api) {
|
|
150
276
|
if (globalThis[FETCH_PATCH_KEY] && globalThis[FETCH_PATCH_KEY].installed) {
|
|
151
277
|
return;
|
|
152
278
|
}
|
|
153
|
-
|
|
279
|
+
const originalFetch = globalThis.fetch;
|
|
154
280
|
if (typeof originalFetch !== "function") {
|
|
155
|
-
api.logger.warn
|
|
281
|
+
api.logger.warn?.("[plugin-deliverables] global fetch is unavailable; split patch skipped");
|
|
156
282
|
return;
|
|
157
283
|
}
|
|
158
284
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
285
|
+
const patchedFetch = async function(input, init) {
|
|
286
|
+
const url = resolveFetchURL(input);
|
|
287
|
+
const method = resolveFetchMethod(input, init);
|
|
162
288
|
if (method !== "POST" || !/\/bot\/send(?:\?|$)/.test(url)) {
|
|
163
289
|
return originalFetch(input, init);
|
|
164
290
|
}
|
|
165
291
|
|
|
166
|
-
|
|
292
|
+
const requestBody = init && init.body;
|
|
167
293
|
if (!isString(requestBody)) {
|
|
168
294
|
return originalFetch(input, init);
|
|
169
295
|
}
|
|
170
296
|
|
|
171
|
-
|
|
297
|
+
let parsed;
|
|
172
298
|
try {
|
|
173
299
|
parsed = JSON.parse(requestBody);
|
|
174
|
-
} catch (
|
|
300
|
+
} catch (_err) {
|
|
175
301
|
return originalFetch(input, init);
|
|
176
302
|
}
|
|
177
303
|
|
|
178
|
-
|
|
304
|
+
const split = shouldSplitPalzPayload(parsed);
|
|
179
305
|
if (!split) {
|
|
180
306
|
return originalFetch(input, init);
|
|
181
307
|
}
|
|
182
308
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
);
|
|
309
|
+
const summaryBody = cloneBody(parsed, split.summary, "__summary");
|
|
310
|
+
const linksBody = cloneBody(parsed, split.links, "__links");
|
|
311
|
+
const summaryBodyStr = JSON.stringify(summaryBody);
|
|
312
|
+
const linksBodyStr = JSON.stringify(linksBody);
|
|
188
313
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
314
|
+
api.logger.info?.(
|
|
315
|
+
`[plugin-deliverables] split deliverable reply for palz target=${String(parsed.conversation_id || "")} summaryMsgId=${String(summaryBody.msg_id || "")} linksMsgId=${String(linksBody.msg_id || "")}`,
|
|
316
|
+
);
|
|
317
|
+
api.logger.info?.(
|
|
318
|
+
`[plugin-deliverables] palz summary request body_length=${summaryBodyStr.length}\n request_body=${summaryBodyStr}`,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const baseInit = Object.assign({}, init);
|
|
322
|
+
const summaryInit = Object.assign({}, baseInit, {
|
|
323
|
+
body: summaryBodyStr,
|
|
192
324
|
});
|
|
193
|
-
|
|
194
|
-
body:
|
|
325
|
+
const linksInit = Object.assign({}, baseInit, {
|
|
326
|
+
body: linksBodyStr,
|
|
195
327
|
});
|
|
196
328
|
|
|
197
|
-
|
|
329
|
+
const firstResponse = await originalFetch(input, summaryInit);
|
|
330
|
+
const firstResponseText = await readResponseText(firstResponse);
|
|
331
|
+
api.logger.info?.(
|
|
332
|
+
`[plugin-deliverables] palz summary response status=${firstResponse ? firstResponse.status : "unknown"} ok=${Boolean(firstResponse && firstResponse.ok)}\n response_body=${previewText(firstResponseText)}`,
|
|
333
|
+
);
|
|
198
334
|
if (!firstResponse || !firstResponse.ok) {
|
|
199
335
|
return firstResponse;
|
|
200
336
|
}
|
|
201
|
-
|
|
337
|
+
|
|
338
|
+
api.logger.info?.(
|
|
339
|
+
`[plugin-deliverables] palz links request body_length=${linksBodyStr.length}\n request_body=${linksBodyStr}`,
|
|
340
|
+
);
|
|
341
|
+
const secondResponse = await originalFetch(input, linksInit);
|
|
342
|
+
const secondResponseText = await readResponseText(secondResponse);
|
|
343
|
+
api.logger.info?.(
|
|
344
|
+
`[plugin-deliverables] palz links response status=${secondResponse ? secondResponse.status : "unknown"} ok=${Boolean(secondResponse && secondResponse.ok)}\n response_body=${previewText(secondResponseText)}`,
|
|
345
|
+
);
|
|
346
|
+
return secondResponse;
|
|
202
347
|
};
|
|
203
348
|
|
|
204
349
|
globalThis.fetch = patchedFetch;
|
|
205
350
|
globalThis[FETCH_PATCH_KEY] = {
|
|
206
351
|
installed: true,
|
|
207
|
-
originalFetch
|
|
352
|
+
originalFetch,
|
|
208
353
|
};
|
|
209
354
|
}
|
|
210
355
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
356
|
+
const plugin = {
|
|
357
|
+
id: "plugin-deliverables",
|
|
358
|
+
name: "Deliverables",
|
|
359
|
+
description: "Upload-first runtime guard for generated file deliverables.",
|
|
360
|
+
register(api) {
|
|
361
|
+
installPalzFetchPatch(api);
|
|
362
|
+
|
|
363
|
+
api.on("before_prompt_build", async () => ({
|
|
364
|
+
prependSystemContext: RUNTIME_DELIVERABLES_GUIDANCE,
|
|
365
|
+
}));
|
|
366
|
+
|
|
367
|
+
api.on("before_tool_call", async (event) => {
|
|
368
|
+
if (isDeliverablesUploadTool(event.toolName)) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!isOutboundMessageTool(event.toolName)) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (!isDirectAttachmentBypass(event.params)) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
api.logger.warn?.(
|
|
378
|
+
`plugin-deliverables: blocked direct file/message bypass via ${event.toolName}`,
|
|
379
|
+
);
|
|
380
|
+
return {
|
|
381
|
+
block: true,
|
|
382
|
+
blockReason:
|
|
383
|
+
"Direct file/message delivery is disabled for deliverables. Write the artifact under output/ and call deliverables__upload_deliverable instead.",
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
api.on("message_sending", async (event, ctx) => {
|
|
388
|
+
const mediaUrls = extractMediaUrls(event.metadata);
|
|
389
|
+
if (mediaUrls.length === 0 && !containsMediaToken(event.content)) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
api.logger.warn?.(
|
|
393
|
+
`plugin-deliverables: cancelled outbound media bypass on ${ctx.channelId || "unknown"} (${mediaUrls.length} media item(s))`,
|
|
394
|
+
);
|
|
395
|
+
return { cancel: true };
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
};
|
|
214
399
|
|
|
215
|
-
module.exports =
|
|
216
|
-
module.exports.default =
|
|
217
|
-
module.exports.id = "plugin-deliverables";
|
|
400
|
+
module.exports = plugin;
|
|
401
|
+
module.exports.default = plugin;
|