@dai_ming/plugin-deliverables 1.0.14 → 1.0.15
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 +215 -0
- package/README.md +186 -90
- package/agents-rules/deliverables.md +30 -34
- package/index.js +118 -186
- 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,217 +1,149 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
"use strict";
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
4
|
+
"## Deliverables Runtime Guard (HARD CONSTRAINT)",
|
|
5
|
+
"",
|
|
6
|
+
"- These rules apply to the main agent and all subagents.",
|
|
7
|
+
"- 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`.",
|
|
8
|
+
"- 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.",
|
|
9
|
+
"- Do not emit `MEDIA:` file references for user-facing deliverables. Deliverable links must come from the deliverables upload tool instead.",
|
|
10
|
+
"- After a successful upload, reply with a short content-aware intro and then preserve the Markdown links returned by the deliverables tool.",
|
|
11
|
+
].join("\n");
|
|
12
|
+
|
|
13
|
+
const ATTACHMENT_PARAM_KEYS = new Set([
|
|
14
|
+
"attachment",
|
|
15
|
+
"attachments",
|
|
16
|
+
"buffer",
|
|
17
|
+
"file",
|
|
18
|
+
"filepath",
|
|
19
|
+
"files",
|
|
20
|
+
"filename",
|
|
21
|
+
"media",
|
|
22
|
+
"mediaurl",
|
|
23
|
+
"mediaurls",
|
|
24
|
+
"path",
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function normalizeToolName(toolName) {
|
|
28
|
+
return typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
|
|
16
29
|
}
|
|
17
30
|
|
|
18
|
-
function
|
|
19
|
-
|
|
31
|
+
function isDeliverablesUploadTool(toolName) {
|
|
32
|
+
const normalized = normalizeToolName(toolName);
|
|
33
|
+
return normalized === "upload_deliverable" || normalized.endsWith("__upload_deliverable");
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
function
|
|
23
|
-
|
|
36
|
+
function isOutboundMessageTool(toolName) {
|
|
37
|
+
const normalized = normalizeToolName(toolName);
|
|
38
|
+
if (!normalized) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
normalized === "message" ||
|
|
43
|
+
normalized.endsWith("__message") ||
|
|
44
|
+
normalized.includes("sendattachment") ||
|
|
45
|
+
normalized.includes("send_attachment")
|
|
46
|
+
);
|
|
24
47
|
}
|
|
25
48
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return "";
|
|
30
|
-
}
|
|
31
|
-
var markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
|
|
32
|
-
if (markdownLink && markdownLink[1]) {
|
|
33
|
-
return markdownLink[1].trim();
|
|
49
|
+
function hasNonEmptyValue(value) {
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
return value.trim().length > 0;
|
|
34
52
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return rawLink[0].trim();
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return value.length > 0;
|
|
38
55
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function isDeliverableLinkLine(line) {
|
|
43
|
-
var text = String(line || "");
|
|
44
|
-
if (/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(text)) {
|
|
45
|
-
return true;
|
|
56
|
+
if (value && typeof value === "object") {
|
|
57
|
+
return Object.keys(value).length > 0;
|
|
46
58
|
}
|
|
47
|
-
return
|
|
59
|
+
return Boolean(value);
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
var lines = normalized.split("\n");
|
|
57
|
-
var firstLinkIndex = -1;
|
|
58
|
-
var i;
|
|
59
|
-
for (i = 0; i < lines.length; i += 1) {
|
|
60
|
-
if (isDeliverableLinkLine(lines[i])) {
|
|
61
|
-
firstLinkIndex = i;
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (firstLinkIndex < 0) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
var summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
|
|
70
|
-
if (summaryLines.length > 0 && isLinksHeading(summaryLines[summaryLines.length - 1])) {
|
|
71
|
-
summaryLines.pop();
|
|
62
|
+
function containsMediaToken(value) {
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
return /(^|\n)\s*MEDIA:\s*\S+/i.test(value);
|
|
72
65
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
var linkLines = [];
|
|
76
|
-
for (i = firstLinkIndex; i < lines.length; i += 1) {
|
|
77
|
-
var line = lines[i];
|
|
78
|
-
if (isLinksHeading(line)) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (isDeliverableLinkLine(line)) {
|
|
82
|
-
var url = extractDeliverableURL(stripBulletPrefix(line));
|
|
83
|
-
if (url) {
|
|
84
|
-
linkLines.push(url);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
var summary = summaryLines.join("\n").trim();
|
|
90
|
-
var links = linkLines.length > 0 ? linkLines[0] : "";
|
|
91
|
-
if (!summary || !links) {
|
|
92
|
-
return null;
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
return value.some(containsMediaToken);
|
|
93
68
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
summary: summary,
|
|
97
|
-
links: links,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function cloneBody(body, content, suffix) {
|
|
102
|
-
var next = {};
|
|
103
|
-
Object.keys(body).forEach(function(key) {
|
|
104
|
-
next[key] = body[key];
|
|
105
|
-
});
|
|
106
|
-
next.content = content;
|
|
107
|
-
if (isString(body.msg_id) && body.msg_id.trim()) {
|
|
108
|
-
next.msg_id = body.msg_id + suffix;
|
|
69
|
+
if (!value || typeof value !== "object") {
|
|
70
|
+
return false;
|
|
109
71
|
}
|
|
110
|
-
return
|
|
72
|
+
return Object.values(value).some(containsMediaToken);
|
|
111
73
|
}
|
|
112
74
|
|
|
113
|
-
function
|
|
114
|
-
if (!
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
if (!isString(body.content)) {
|
|
118
|
-
return null;
|
|
75
|
+
function isDirectAttachmentBypass(params) {
|
|
76
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
77
|
+
return false;
|
|
119
78
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return null;
|
|
79
|
+
for (const [key, value] of Object.entries(params)) {
|
|
80
|
+
if (ATTACHMENT_PARAM_KEYS.has(String(key).trim().toLowerCase()) && hasNonEmptyValue(value)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
125
83
|
}
|
|
126
|
-
return
|
|
84
|
+
return containsMediaToken(params);
|
|
127
85
|
}
|
|
128
86
|
|
|
129
|
-
function
|
|
130
|
-
if (
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
if (input && isString(input.url)) {
|
|
134
|
-
return input.url;
|
|
87
|
+
function extractMediaUrls(metadata) {
|
|
88
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
89
|
+
return [];
|
|
135
90
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
91
|
+
const urls = [];
|
|
92
|
+
const mediaUrls = metadata.mediaUrls;
|
|
93
|
+
if (Array.isArray(mediaUrls)) {
|
|
94
|
+
for (const entry of mediaUrls) {
|
|
95
|
+
if (typeof entry === "string" && entry.trim()) {
|
|
96
|
+
urls.push(entry.trim());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
142
99
|
}
|
|
143
|
-
if (
|
|
144
|
-
|
|
100
|
+
if (typeof metadata.mediaUrl === "string" && metadata.mediaUrl.trim()) {
|
|
101
|
+
urls.push(metadata.mediaUrl.trim());
|
|
145
102
|
}
|
|
146
|
-
return
|
|
103
|
+
return urls;
|
|
147
104
|
}
|
|
148
105
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
var parsed;
|
|
172
|
-
try {
|
|
173
|
-
parsed = JSON.parse(requestBody);
|
|
174
|
-
} catch (err) {
|
|
175
|
-
return originalFetch(input, init);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
var split = shouldSplitPalzPayload(parsed);
|
|
179
|
-
if (!split) {
|
|
180
|
-
return originalFetch(input, init);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
api.logger.info &&
|
|
184
|
-
api.logger.info(
|
|
185
|
-
"[plugin-deliverables] split deliverable reply for palz target=" +
|
|
186
|
-
String(parsed.conversation_id || ""),
|
|
106
|
+
const plugin = {
|
|
107
|
+
id: "plugin-deliverables",
|
|
108
|
+
name: "Deliverables",
|
|
109
|
+
description: "Upload-first runtime guard for generated file deliverables.",
|
|
110
|
+
register(api) {
|
|
111
|
+
api.on("before_prompt_build", async () => ({
|
|
112
|
+
prependSystemContext: RUNTIME_DELIVERABLES_GUIDANCE,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
api.on("before_tool_call", async (event) => {
|
|
116
|
+
if (isDeliverablesUploadTool(event.toolName)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!isOutboundMessageTool(event.toolName)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!isDirectAttachmentBypass(event.params)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
api.logger.warn?.(
|
|
126
|
+
`plugin-deliverables: blocked direct file/message bypass via ${event.toolName}`,
|
|
187
127
|
);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
var linksInit = Object.assign({}, baseInit, {
|
|
194
|
-
body: JSON.stringify(cloneBody(parsed, split.links, "__links")),
|
|
128
|
+
return {
|
|
129
|
+
block: true,
|
|
130
|
+
blockReason:
|
|
131
|
+
"Direct file/message delivery is disabled for deliverables. Write the artifact under output/ and call deliverables__upload_deliverable instead.",
|
|
132
|
+
};
|
|
195
133
|
});
|
|
196
134
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function register(api) {
|
|
212
|
-
installPalzFetchPatch(api);
|
|
213
|
-
}
|
|
135
|
+
api.on("message_sending", async (event, ctx) => {
|
|
136
|
+
const mediaUrls = extractMediaUrls(event.metadata);
|
|
137
|
+
if (mediaUrls.length === 0 && !containsMediaToken(event.content)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
api.logger.warn?.(
|
|
141
|
+
`plugin-deliverables: cancelled outbound media bypass on ${ctx.channelId || "unknown"} (${mediaUrls.length} media item(s))`,
|
|
142
|
+
);
|
|
143
|
+
return { cancel: true };
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
214
147
|
|
|
215
|
-
module.exports =
|
|
216
|
-
module.exports.default =
|
|
217
|
-
module.exports.id = "plugin-deliverables";
|
|
148
|
+
module.exports = plugin;
|
|
149
|
+
module.exports.default = plugin;
|