@dai_ming/plugin-deliverables 1.0.10 → 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 +188 -86
- package/agents-rules/deliverables.md +22 -44
- package/index.js +149 -0
- package/mcp-servers/deliverables.js +26 -308
- package/openclaw-plugin.json +2 -4
- package/openclaw.plugin.json +12 -0
- package/package.json +10 -2
- package/skills/deliverables/SKILL.md +11 -25
- package/install.js +0 -463
package/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
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() : "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isDeliverablesUploadTool(toolName) {
|
|
32
|
+
const normalized = normalizeToolName(toolName);
|
|
33
|
+
return normalized === "upload_deliverable" || normalized.endsWith("__upload_deliverable");
|
|
34
|
+
}
|
|
35
|
+
|
|
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
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasNonEmptyValue(value) {
|
|
50
|
+
if (typeof value === "string") {
|
|
51
|
+
return value.trim().length > 0;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
return value.length > 0;
|
|
55
|
+
}
|
|
56
|
+
if (value && typeof value === "object") {
|
|
57
|
+
return Object.keys(value).length > 0;
|
|
58
|
+
}
|
|
59
|
+
return Boolean(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function containsMediaToken(value) {
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
return /(^|\n)\s*MEDIA:\s*\S+/i.test(value);
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
return value.some(containsMediaToken);
|
|
68
|
+
}
|
|
69
|
+
if (!value || typeof value !== "object") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return Object.values(value).some(containsMediaToken);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isDirectAttachmentBypass(params) {
|
|
76
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
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
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return containsMediaToken(params);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function extractMediaUrls(metadata) {
|
|
88
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
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
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (typeof metadata.mediaUrl === "string" && metadata.mediaUrl.trim()) {
|
|
101
|
+
urls.push(metadata.mediaUrl.trim());
|
|
102
|
+
}
|
|
103
|
+
return urls;
|
|
104
|
+
}
|
|
105
|
+
|
|
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}`,
|
|
127
|
+
);
|
|
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
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
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
|
+
};
|
|
147
|
+
|
|
148
|
+
module.exports = plugin;
|
|
149
|
+
module.exports.default = plugin;
|
|
@@ -14,15 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
var http = require("http");
|
|
16
16
|
var https = require("https");
|
|
17
|
-
var path = require("path");
|
|
18
17
|
|
|
19
18
|
var GATEWAY_URL = (process.env.CLAW_GATEWAY_URL || "http://claw-gateway:8080").replace(/\/$/, "");
|
|
20
|
-
var
|
|
19
|
+
var GATEWAY_PUBLIC = (process.env.CLAW_GATEWAY_PUBLIC_URL || GATEWAY_URL).replace(/\/$/, "");
|
|
21
20
|
// Some existing pods were created without CLAW_GATEWAY_API_KEY injected.
|
|
22
21
|
// Keep env-driven behavior first, but provide a dev-compatible fallback to avoid
|
|
23
22
|
// breaking deliverable uploads during rolling migration.
|
|
24
23
|
var API_KEY = process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY || "api-key-1";
|
|
25
|
-
var GATEWAY_PUBLIC = resolveGatewayPublicBase();
|
|
26
24
|
|
|
27
25
|
var TOOL_DEFS = [
|
|
28
26
|
{
|
|
@@ -56,7 +54,7 @@ var TOOL_DEFS = [
|
|
|
56
54
|
},
|
|
57
55
|
file_name: {
|
|
58
56
|
type: "string",
|
|
59
|
-
description: "
|
|
57
|
+
description: "用户可见的文件名,例如 adventure-game 或 report.md。多文件交付时这里应是目录名/项目名,不要写成 .zip,除非用户明确要求压缩包。"
|
|
60
58
|
},
|
|
61
59
|
content_text: {
|
|
62
60
|
type: "string",
|
|
@@ -95,269 +93,6 @@ function parseURL(rawURL) {
|
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
function isPrivateIPv4(hostname) {
|
|
99
|
-
if (!hostname) return false;
|
|
100
|
-
if (/^10\./.test(hostname)) return true;
|
|
101
|
-
if (/^127\./.test(hostname)) return true;
|
|
102
|
-
if (/^192\.168\./.test(hostname)) return true;
|
|
103
|
-
var m = hostname.match(/^172\.(\d+)\./);
|
|
104
|
-
if (m) {
|
|
105
|
-
var second = parseInt(m[1], 10);
|
|
106
|
-
return second >= 16 && second <= 31;
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isInternalHostname(hostname) {
|
|
112
|
-
var host = String(hostname || "").toLowerCase();
|
|
113
|
-
if (!host) return false;
|
|
114
|
-
if (host === "localhost" || host === "claw-gateway") return true;
|
|
115
|
-
if (host.indexOf(".svc") >= 0 || host.indexOf(".cluster.local") >= 0) return true;
|
|
116
|
-
if (host.indexOf("claw-gateway:") === 0) return true;
|
|
117
|
-
if (isPrivateIPv4(host)) return true;
|
|
118
|
-
if (host.indexOf(".") < 0) return true;
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function isProbablyInternalURL(rawURL) {
|
|
123
|
-
var parsed = parseURL(rawURL);
|
|
124
|
-
if (!parsed) return false;
|
|
125
|
-
return isInternalHostname(parsed.hostname);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function envGatewayPublicFallback() {
|
|
129
|
-
var helmEnv = String(process.env.HELM_ENV || process.env.ENV || "").toLowerCase();
|
|
130
|
-
if (helmEnv === "prod" || helmEnv === "production" || helmEnv === "online") {
|
|
131
|
-
return "https://claw-gateway.csagentai.com";
|
|
132
|
-
}
|
|
133
|
-
if (helmEnv === "dev" || helmEnv === "development" || helmEnv === "staging" || helmEnv === "stage") {
|
|
134
|
-
return "https://claw-dev.csaiagent.com";
|
|
135
|
-
}
|
|
136
|
-
return "";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function resolveGatewayPublicBase() {
|
|
140
|
-
var candidate = RAW_GATEWAY_PUBLIC;
|
|
141
|
-
if (candidate && !isProbablyInternalURL(candidate)) {
|
|
142
|
-
return candidate;
|
|
143
|
-
}
|
|
144
|
-
var fallback = envGatewayPublicFallback();
|
|
145
|
-
if (fallback) {
|
|
146
|
-
return fallback.replace(/\/$/, "");
|
|
147
|
-
}
|
|
148
|
-
return candidate;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function absolutizePublicURL(rawURL) {
|
|
152
|
-
var val = String(rawURL || "").trim();
|
|
153
|
-
if (!val) return "";
|
|
154
|
-
if (/^https?:\/\//i.test(val)) {
|
|
155
|
-
if (isProbablyInternalURL(val) && GATEWAY_PUBLIC && !isProbablyInternalURL(GATEWAY_PUBLIC)) {
|
|
156
|
-
var parsed = parseURL(val);
|
|
157
|
-
if (parsed) {
|
|
158
|
-
return GATEWAY_PUBLIC + parsed.pathname + (parsed.search || "") + (parsed.hash || "");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return val;
|
|
162
|
-
}
|
|
163
|
-
if (val.charAt(0) !== "/") {
|
|
164
|
-
val = "/" + val;
|
|
165
|
-
}
|
|
166
|
-
return (GATEWAY_PUBLIC || RAW_GATEWAY_PUBLIC || "").replace(/\/$/, "") + val;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function outputPublicBase() {
|
|
170
|
-
return absolutizePublicURL("/openclaw-gateway/output");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function encodePathSegments(parts) {
|
|
174
|
-
return parts.map(function(part) {
|
|
175
|
-
return encodeURIComponent(String(part || ""));
|
|
176
|
-
}).join("/");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function sanitizePathSegment(v) {
|
|
180
|
-
var s = String(v || "").trim();
|
|
181
|
-
if (!s) return "unknown";
|
|
182
|
-
s = s.replace(/\//g, "_");
|
|
183
|
-
s = s.replace(/\\/g, "_");
|
|
184
|
-
s = s.replace(/\.\./g, "_");
|
|
185
|
-
return s;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function sanitizeFileName(v) {
|
|
189
|
-
var name = String(v || "").trim();
|
|
190
|
-
if (!name) return "file";
|
|
191
|
-
name = name.replace(/^.*[\/\\]/, "");
|
|
192
|
-
name = name.replace(/\//g, "_");
|
|
193
|
-
name = name.replace(/\\/g, "_");
|
|
194
|
-
name = name.replace(/\.\./g, "_");
|
|
195
|
-
if (!name || name === ".") return "file";
|
|
196
|
-
return name;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function fileExtension(name) {
|
|
200
|
-
return path.extname(String(name || ""));
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function hasFileExtension(name) {
|
|
204
|
-
return !!fileExtension(name);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function looksLikeHTML(text) {
|
|
208
|
-
var s = String(text || "").trim().toLowerCase();
|
|
209
|
-
if (!s) return false;
|
|
210
|
-
return s.indexOf("<!doctype html") === 0 ||
|
|
211
|
-
s.indexOf("<html") === 0 ||
|
|
212
|
-
s.indexOf("<head") === 0 ||
|
|
213
|
-
s.indexOf("<body") === 0;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function looksLikeMarkdown(text) {
|
|
217
|
-
var s = String(text || "").trim();
|
|
218
|
-
if (!s) return false;
|
|
219
|
-
return /^#{1,6}\s+\S/m.test(s) ||
|
|
220
|
-
/^\s*[-*+]\s+\S/m.test(s) ||
|
|
221
|
-
/^\s*\d+\.\s+\S/m.test(s) ||
|
|
222
|
-
/\[[^\]]+\]\([^)]+\)/.test(s) ||
|
|
223
|
-
/```/.test(s);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function defaultExtensionForDeliverable(type, contentText, files) {
|
|
227
|
-
var kind = String(type || "").trim().toLowerCase();
|
|
228
|
-
if (files && files.length > 0) return "";
|
|
229
|
-
switch (kind) {
|
|
230
|
-
case "article":
|
|
231
|
-
if (looksLikeHTML(contentText)) return ".html";
|
|
232
|
-
return ".md";
|
|
233
|
-
case "game":
|
|
234
|
-
return ".html";
|
|
235
|
-
case "image":
|
|
236
|
-
return ".png";
|
|
237
|
-
case "video":
|
|
238
|
-
return ".mp4";
|
|
239
|
-
case "zip":
|
|
240
|
-
return ".zip";
|
|
241
|
-
case "ppt":
|
|
242
|
-
return ".pptx";
|
|
243
|
-
case "link":
|
|
244
|
-
return "";
|
|
245
|
-
default:
|
|
246
|
-
if (looksLikeHTML(contentText)) return ".html";
|
|
247
|
-
if (looksLikeMarkdown(contentText)) return ".md";
|
|
248
|
-
return "";
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function normalizeDeliverableFileName(fileName, type, contentText, files) {
|
|
253
|
-
if (String(type || "").trim().toLowerCase() === "link") {
|
|
254
|
-
return String(fileName || "").trim();
|
|
255
|
-
}
|
|
256
|
-
var normalized = sanitizeFileName(fileName);
|
|
257
|
-
if (files && files.length > 0) {
|
|
258
|
-
return normalized || "deliverable";
|
|
259
|
-
}
|
|
260
|
-
if (hasFileExtension(normalized)) {
|
|
261
|
-
return normalized;
|
|
262
|
-
}
|
|
263
|
-
var ext = defaultExtensionForDeliverable(type, contentText, files);
|
|
264
|
-
if (!normalized) normalized = "deliverable";
|
|
265
|
-
return normalized + ext;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function extractUserIDFromResource(resourceID) {
|
|
269
|
-
var trimmed = String(resourceID || "").trim();
|
|
270
|
-
var prefix = "user_";
|
|
271
|
-
if (!trimmed || trimmed.indexOf(prefix) !== 0) {
|
|
272
|
-
return "";
|
|
273
|
-
}
|
|
274
|
-
var rest = trimmed.slice(prefix.length);
|
|
275
|
-
var end = rest.indexOf("_");
|
|
276
|
-
if (end <= 0) {
|
|
277
|
-
return "";
|
|
278
|
-
}
|
|
279
|
-
return rest.slice(0, end);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function deriveRelease(release, resourceID, userID) {
|
|
283
|
-
var rel = sanitizePathSegment(release);
|
|
284
|
-
if (rel && rel !== "unknown") {
|
|
285
|
-
return rel;
|
|
286
|
-
}
|
|
287
|
-
var uid = String(userID || "").trim();
|
|
288
|
-
if (!uid) {
|
|
289
|
-
uid = extractUserIDFromResource(resourceID);
|
|
290
|
-
}
|
|
291
|
-
if (uid) {
|
|
292
|
-
return "user-" + uid;
|
|
293
|
-
}
|
|
294
|
-
return "unknown";
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function looksLikePreviewRoute(rawURL) {
|
|
298
|
-
var val = String(rawURL || "").trim();
|
|
299
|
-
return /\/openclaw-gateway\/preview\/[^/?#]+(?:[?#].*)?$/i.test(val);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function needsOutputURLRepair(data) {
|
|
303
|
-
if (!data) return false;
|
|
304
|
-
if (String(data.backend || "").toLowerCase() !== "output") {
|
|
305
|
-
return false;
|
|
306
|
-
}
|
|
307
|
-
var previewURL = String(data.previewUrl || "").trim();
|
|
308
|
-
var downloadURL = String(data.downloadUrl || "").trim();
|
|
309
|
-
if (!previewURL || !downloadURL) {
|
|
310
|
-
return true;
|
|
311
|
-
}
|
|
312
|
-
if (previewURL.charAt(0) === "/" || downloadURL.charAt(0) === "/") {
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
if (isProbablyInternalURL(previewURL) || isProbablyInternalURL(downloadURL)) {
|
|
316
|
-
return true;
|
|
317
|
-
}
|
|
318
|
-
if (looksLikePreviewRoute(previewURL) || looksLikePreviewRoute(downloadURL)) {
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
return false;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function fetchDeliverableMeta(uuid) {
|
|
325
|
-
if (!uuid) return Promise.resolve(null);
|
|
326
|
-
return httpRequest("GET", "/openclaw-gateway/be/deliverables/" + encodeURIComponent(uuid)).then(function(resp) {
|
|
327
|
-
return resp.body.data || resp.body || null;
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function buildOutputURLs(meta, args, body, data) {
|
|
332
|
-
var base = outputPublicBase();
|
|
333
|
-
if (!base) {
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
var resourceID = sanitizePathSegment((meta && meta.resourceId) || body.resourceId || args.resource_id);
|
|
337
|
-
var userID = (meta && meta.userId) || body.userId || args.user_id;
|
|
338
|
-
var release = deriveRelease((meta && meta.release) || body.release, resourceID, userID);
|
|
339
|
-
var uuid = String((meta && meta.uuid) || (data && data.uuid) || "").trim();
|
|
340
|
-
if (!resourceID || !uuid) {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
var isDirectory = !!(args.files && args.files.length > 0) || String((meta && meta.mime) || "").trim() === "application/x-directory";
|
|
344
|
-
if (isDirectory) {
|
|
345
|
-
var dirURL = base.replace(/\/$/, "") + "/" + encodePathSegments([release, resourceID, uuid]);
|
|
346
|
-
return {
|
|
347
|
-
previewURL: dirURL,
|
|
348
|
-
downloadURL: dirURL + "?list=1",
|
|
349
|
-
isDirectory: true
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
var fileName = sanitizeFileName((meta && meta.fileName) || body.fileName || args.file_name);
|
|
353
|
-
var fileURL = base.replace(/\/$/, "") + "/" + encodePathSegments([release, resourceID, uuid, fileName]);
|
|
354
|
-
return {
|
|
355
|
-
previewURL: fileURL,
|
|
356
|
-
downloadURL: fileURL,
|
|
357
|
-
isDirectory: false
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
96
|
function httpRequest(method, path, body) {
|
|
362
97
|
return new Promise(function(resolve, reject) {
|
|
363
98
|
var parsed = parseURL(GATEWAY_URL + path);
|
|
@@ -432,27 +167,15 @@ function buildReplyMarkdown(opts) {
|
|
|
432
167
|
return lines.join("\n");
|
|
433
168
|
}
|
|
434
169
|
|
|
435
|
-
function resolveReplyURLs(data, args, body) {
|
|
436
|
-
if (!needsOutputURLRepair(data)) {
|
|
437
|
-
return Promise.resolve(null);
|
|
438
|
-
}
|
|
439
|
-
return fetchDeliverableMeta(data.uuid).catch(function() {
|
|
440
|
-
return null;
|
|
441
|
-
}).then(function(meta) {
|
|
442
|
-
return buildOutputURLs(meta, args, body, data);
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
170
|
// ─── Tool implementations ─────────────────────────────────────────────────────
|
|
447
171
|
|
|
448
172
|
function uploadDeliverable(args) {
|
|
449
|
-
var finalFileName = normalizeDeliverableFileName(args.file_name, args.type, args.content_text, args.files);
|
|
450
173
|
var body = {
|
|
451
174
|
resourceId: args.resource_id,
|
|
452
175
|
groupId: args.group_id,
|
|
453
176
|
userId: args.user_id,
|
|
454
177
|
type: args.type,
|
|
455
|
-
fileName:
|
|
178
|
+
fileName: args.file_name,
|
|
456
179
|
release: "" // overwritten below after release name derivation
|
|
457
180
|
};
|
|
458
181
|
|
|
@@ -482,34 +205,29 @@ function uploadDeliverable(args) {
|
|
|
482
205
|
|
|
483
206
|
return httpRequest("POST", "/openclaw-gateway/be/deliverables", body).then(function(resp) {
|
|
484
207
|
var d = resp.body.data || resp.body;
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
type: args.type,
|
|
498
|
-
isDirectory: isDirectory
|
|
499
|
-
});
|
|
500
|
-
return {
|
|
501
|
-
uuid: d.uuid,
|
|
502
|
-
file_name: finalFileName,
|
|
503
|
-
backend: d.backend || "",
|
|
504
|
-
download_url: downloadURL,
|
|
505
|
-
preview_url: previewURL,
|
|
506
|
-
expire_at: d.expireAt,
|
|
507
|
-
is_directory: isDirectory,
|
|
508
|
-
reply_markdown: replyMarkdown,
|
|
509
|
-
trace_id: resp.traceID || "",
|
|
510
|
-
message: replyMarkdown
|
|
511
|
-
};
|
|
208
|
+
// Prefer backend-aware previewUrl returned by gateway (OSS/output/link).
|
|
209
|
+
// Fallback to legacy gateway preview endpoint for compatibility.
|
|
210
|
+
var previewURL = d.previewUrl || (GATEWAY_PUBLIC + "/openclaw-gateway/preview/" + d.uuid);
|
|
211
|
+
var isDirectory = !!(args.files && args.files.length > 0);
|
|
212
|
+
if (!isDirectory && d.downloadUrl && previewURL && d.downloadUrl !== previewURL && /(?:\?|&)list=1(?:&|$)/.test(d.downloadUrl)) {
|
|
213
|
+
isDirectory = true;
|
|
214
|
+
}
|
|
215
|
+
var replyMarkdown = buildReplyMarkdown({
|
|
216
|
+
previewURL: previewURL,
|
|
217
|
+
downloadURL: d.downloadUrl,
|
|
218
|
+
type: args.type,
|
|
219
|
+
isDirectory: isDirectory
|
|
512
220
|
});
|
|
221
|
+
return {
|
|
222
|
+
uuid: d.uuid,
|
|
223
|
+
backend: d.backend || "",
|
|
224
|
+
download_url: d.downloadUrl,
|
|
225
|
+
preview_url: previewURL,
|
|
226
|
+
expire_at: d.expireAt,
|
|
227
|
+
reply_markdown: replyMarkdown,
|
|
228
|
+
trace_id: resp.traceID || "",
|
|
229
|
+
message: replyMarkdown
|
|
230
|
+
};
|
|
513
231
|
});
|
|
514
232
|
}
|
|
515
233
|
|
|
@@ -534,7 +252,7 @@ function handleMessage(msg) {
|
|
|
534
252
|
send({ jsonrpc: "2.0", id: id, result: {
|
|
535
253
|
protocolVersion: "2024-11-05",
|
|
536
254
|
capabilities: { tools: {} },
|
|
537
|
-
serverInfo: { name: "deliverables", version: "1.0.
|
|
255
|
+
serverInfo: { name: "deliverables", version: "1.0.2" }
|
|
538
256
|
}});
|
|
539
257
|
return Promise.resolve();
|
|
540
258
|
|
package/openclaw-plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugin-deliverables",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"npm_package": "@dai_ming/plugin-deliverables",
|
|
5
5
|
"description": "Deliverables plugin: MCP server + skill + AGENTS rules for AI-generated file uploads",
|
|
6
6
|
"mcp_servers": {
|
|
@@ -26,9 +26,7 @@
|
|
|
26
26
|
"openclaw_config": {
|
|
27
27
|
"plugins": {
|
|
28
28
|
"entries": {
|
|
29
|
-
"plugin-deliverables": {
|
|
30
|
-
"enabled": true
|
|
31
|
-
}
|
|
29
|
+
"plugin-deliverables": {}
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "plugin-deliverables",
|
|
3
|
+
"name": "Deliverables",
|
|
4
|
+
"description": "Deliverables runtime guard for upload-first file delivery.",
|
|
5
|
+
"version": "1.0.15",
|
|
6
|
+
"skills": ["./skills"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {}
|
|
11
|
+
}
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dai_ming/plugin-deliverables",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -9,13 +9,21 @@
|
|
|
9
9
|
"mcp"
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
|
+
"main": "index.js",
|
|
12
13
|
"files": [
|
|
13
|
-
"
|
|
14
|
+
"INSTALL.md",
|
|
15
|
+
"index.js",
|
|
16
|
+
"openclaw.plugin.json",
|
|
14
17
|
"openclaw-plugin.json",
|
|
15
18
|
"mcp-servers/",
|
|
16
19
|
"skills/",
|
|
17
20
|
"agents-rules/"
|
|
18
21
|
],
|
|
22
|
+
"openclaw": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./index.js"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
19
27
|
"engines": {
|
|
20
28
|
"node": ">=16"
|
|
21
29
|
}
|
|
@@ -21,8 +21,6 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
21
21
|
- 多文件内容(游戏/网站)写到 `output/<目录名>/...`,保持目录结构。
|
|
22
22
|
- 如果你使用 `write` 工具创建交付物文件,目标路径必须在 `output/` 下。
|
|
23
23
|
- 如果你已经误写到工作区根目录,必须先移动或复制到 `output/`,再继续上传交付物并向用户汇报。
|
|
24
|
-
- 交付物文件名必须保留原始扩展名,不要丢后缀。示例:`刘德华介绍.md`、`周杰伦.md`、`996专题.html`。
|
|
25
|
-
- 如果用户要求 Markdown,就保留 `.md`;如果用户要求 HTML,就保留 `.html`;如果是纯文本,就保留 `.txt`。
|
|
26
24
|
|
|
27
25
|
## 调用前必须准备的参数(全部必填)
|
|
28
26
|
|
|
@@ -34,7 +32,7 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
34
32
|
| `group_id` | 消息元数据中的 `group_id` 或 `conversation_id` | `group_abc123` |
|
|
35
33
|
| `user_id` | 消息元数据中的 `sender_id` 或 `owner_id` | `cbb0fab9...` |
|
|
36
34
|
| `type` | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`zip`/`link` | `article` |
|
|
37
|
-
| `file_name` |
|
|
35
|
+
| `file_name` | 有意义的文件名,含扩展名 | `report-2026.html` |
|
|
38
36
|
| `content_text` | 文件的完整文本内容(HTML/Markdown等) | `<html>...</html>` |
|
|
39
37
|
|
|
40
38
|
> **直接对话(direct chat)时**:`group_id` 填 `conversation_id`,`user_id` 填 `owner_id`。
|
|
@@ -59,25 +57,13 @@ files: [
|
|
|
59
57
|
## 上传成功后
|
|
60
58
|
|
|
61
59
|
优先规则:
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- 第一条介绍里不要放工作区路径、裸链接、Markdown 链接或下载提示。
|
|
73
|
-
- 第二条链接回复里,如果工具结果有 `reply_markdown`,直接原样输出它,不要改写,不要再加标题或说明。
|
|
74
|
-
- 第二条链接回复只允许包含:
|
|
75
|
-
- `预览链接:[点击预览](...)`
|
|
76
|
-
- `下载链接:[点击下载](...)`
|
|
77
|
-
- 或目录场景下的 `文件列表:[查看目录](...)`
|
|
78
|
-
- 不要把第一条和第二条混在一起,也不要在第二条链接消息里再补“已完成”“请查看”等多余文字。
|
|
79
|
-
|
|
80
|
-
回退规则:
|
|
81
|
-
- 如果当前会话没有 `message` 工具,再退回成一条最终 Markdown 回复:先介绍,再附链接。
|
|
82
|
-
|
|
83
|
-
不要在消息里输出文件的完整内容、工作区保存路径或裸链接,也不要只输出“一句简介 + 两个链接”这种过短格式。
|
|
60
|
+
- 最终消息必须先用你自己的话写 1-2 句简短介绍,粗略说明你生成了什么内容、包含哪些重点。
|
|
61
|
+
- 这句介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
|
|
62
|
+
- 如果工具结果里有 `reply_markdown`,把它原样放在这句介绍后面,不要改写其中的链接。
|
|
63
|
+
|
|
64
|
+
否则只发给用户:
|
|
65
|
+
- 预览链接(`preview_url`,必须用 Markdown 链接格式)
|
|
66
|
+
- 下载链接(`download_url`,必须用 Markdown 链接格式)
|
|
67
|
+
- 多文件/游戏目录场景下,第二行也可以是 `文件列表:[查看目录](...)`,不要改成裸 URL 或 zip 描述。
|
|
68
|
+
|
|
69
|
+
不要在消息里输出文件的完整内容、工作区保存路径或裸链接。
|