@dai_ming/plugin-deliverables 1.0.6 → 1.0.9

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 CHANGED
@@ -20,7 +20,7 @@ OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则
20
20
 
21
21
  ```yaml
22
22
  installPlugins:
23
- - "@dai_ming/plugin-deliverables@1.0.6"
23
+ - "@dai_ming/plugin-deliverables@1.0.9"
24
24
  ```
25
25
 
26
26
  现有 chart 会在 init 阶段完成安装。
@@ -31,7 +31,7 @@ installPlugins:
31
31
 
32
32
  ```bash
33
33
  npm config set registry https://registry.npmmirror.com
34
- npm install @dai_ming/plugin-deliverables@1.0.6
34
+ npm install @dai_ming/plugin-deliverables@1.0.9
35
35
  node node_modules/@dai_ming/plugin-deliverables/install.js
36
36
  ```
37
37
 
@@ -133,3 +133,7 @@ env | grep -E 'CLAW_GATEWAY|OPENCLAW_GATEWAY|botID'
133
133
  ```bash
134
134
  curl -H "X-API-Key: $CLAW_GATEWAY_API_KEY" "$CLAW_GATEWAY_URL/healthz"
135
135
  ```
136
+
137
+ ### 返回的是 `/preview/:uuid` 或内部地址
138
+
139
+ `1.0.9` 起,插件会在 `output` 交付物场景下自动把相对/内部 preview 地址修正为最终的外部 `output` 直链,不再依赖 gateway 当前环境里是否显式配置了 `deliverable.gatewayPublicUrl`。
@@ -12,25 +12,32 @@ Tool name note:
12
12
 
13
13
  ### Required output behavior
14
14
 
15
- 0. Your final assistant message MUST start with 1-2 short sentences in your own words, briefly describing what you produced for the user.
15
+ 0. Your final assistant message MUST start with a richer Markdown intro section, not a single short confirmation sentence.
16
16
  1. The intro must be based on the actual content/request, not a fixed boilerplate like `交付物已上传成功,可直接在线预览或下载。`
17
- 2. If tool result contains `reply_markdown`, append that field verbatim after your intro on the following lines. Do not rewrite the links inside it.
18
- 3. If tool result has `preview_url`, include one line:
17
+ 2. The intro SHOULD use Markdown structure, preferably:
18
+ - a short title or summary line
19
+ - a `### 内容概览` section
20
+ - 3-6 bullet points describing the actual sections, highlights, features, or focus of the deliverable
21
+ 3. The intro should be noticeably more informative than one sentence. For normal articles/reports/introductions, aim for roughly 80-200 Chinese characters total before the links.
22
+ 4. The bullet points must describe the real deliverable content. Do not use empty filler like "内容丰富" / "结构清晰" / "值得阅读" unless paired with concrete details.
23
+ 5. If tool result contains `reply_markdown`, append a `### 访问链接` heading and then append that field verbatim on the following lines. Do not rewrite the links inside it.
24
+ 6. If tool result has `preview_url`, include one line:
19
25
  `预览链接:[点击预览](<full_url>)`
20
- 4. If tool result has `download_url`, include one line:
26
+ 7. If tool result has `download_url`, include one line:
21
27
  `下载链接:[点击下载](<full_url>)`
22
- 5. For multi-file/game deliverables, if tool result already formats the second line as
28
+ 8. For multi-file/game deliverables, if tool result already formats the second line as
23
29
  `文件列表:[查看目录](<full_url>)`
24
30
  then keep that exact label and URL. Do not rewrite it back to a raw long link.
25
- 6. Keep URL target value exactly from tool result (no shortening, no masking, no redirect rewrite).
26
- 7. Use short link labels (`点击预览` / `点击下载` / `查看目录`) to avoid exposing long raw URLs.
27
- 8. Do not say "已上传/已完成" without links.
28
- 9. Do not output naked long URLs outside Markdown link syntax.
29
- 10. Keep the intro concise; do not paste the full file content, saved-path text, or a long summary after the Markdown links.
31
+ 9. Keep URL target value exactly from tool result (no shortening, no masking, no redirect rewrite).
32
+ 10. Use short link labels (`点击预览` / `点击下载` / `查看目录`) to avoid exposing long raw URLs.
33
+ 11. Do not say "已上传/已完成" without links.
34
+ 12. Do not output naked long URLs outside Markdown link syntax.
35
+ 13. Keep the intro concise but substantive; do not paste the full file content, saved-path text, or a huge essay after the Markdown links.
30
36
 
31
37
  ### Forbidden output behavior
32
38
 
33
39
  - "可点击预览链接查看" but no actual URL
40
+ - A single short sentence plus links when the deliverable is an article/report/introduction and concrete highlights are available
34
41
  - Replacing URL target with a non-original URL
35
42
  - Omitting both links when tool already returned links
36
43
  - Outputting only naked long URL without Markdown link label
@@ -59,17 +66,22 @@ Tool name note:
59
66
  6. For multi-file deliverables (game/site), you MUST prefer `type=game` with `files[]`, keep files as a folder structure, and do NOT zip before upload unless the user explicitly asks for a zip package.
60
67
  7. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
61
68
  8. If a generated project requires starting a separate backend service, custom port, database, or long-running process to work, do NOT pretend the deliverables preview can run it. Tell the user that deliverables preview only supports static output, and that runtime projects need deployment/ingress instead.
62
- 9. After successful upload, the final assistant message MUST start with 1-2 short sentences in your own words, briefly describing what you generated for the user.
69
+ 9. After successful upload, the final assistant message MUST start with a richer Markdown intro section, not just one short sentence.
63
70
  10. The intro must be based on the actual deliverable content/request, not a fixed sentence like `交付物已上传成功,可直接在线预览或下载。`
64
- 11. If tool result contains `reply_markdown`, append that field verbatim after your intro on the following lines.
65
- 12. Otherwise final reply MUST include Markdown links (short label + full URL target):
71
+ 11. Prefer this Markdown structure:
72
+ - a short title or summary line
73
+ - `### 内容概览`
74
+ - 3-6 bullet points summarizing the actual content sections, highlights, or key features
75
+ 12. For ordinary articles/reports/introductions, the intro before the links should normally reach roughly 80-200 Chinese characters total.
76
+ 13. If tool result contains `reply_markdown`, add `### 访问链接` and append that field verbatim on the following lines.
77
+ 14. Otherwise final reply MUST include Markdown links (short label + full URL target):
66
78
  `预览链接:[点击预览](<full_url>)`
67
79
  `下载链接:[点击下载](<full_url>)`
68
- 13. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
80
+ 15. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
69
81
  `文件列表:[查看目录](<full_url>)`
70
82
  Keep that format instead of forcing a zip link.
71
- 14. Do NOT only say "已保存到工作空间".
72
- 15. Do NOT append workspace path or a raw URL block after the Markdown links.
83
+ 16. Do NOT only say "已保存到工作空间".
84
+ 17. Do NOT append workspace path or a raw URL block after the Markdown links.
73
85
 
74
86
  ### Exception
75
87
 
@@ -16,11 +16,12 @@ var http = require("http");
16
16
  var https = require("https");
17
17
 
18
18
  var GATEWAY_URL = (process.env.CLAW_GATEWAY_URL || "http://claw-gateway:8080").replace(/\/$/, "");
19
- var GATEWAY_PUBLIC = (process.env.CLAW_GATEWAY_PUBLIC_URL || GATEWAY_URL).replace(/\/$/, "");
19
+ var RAW_GATEWAY_PUBLIC = (process.env.CLAW_GATEWAY_PUBLIC_URL || GATEWAY_URL).replace(/\/$/, "");
20
20
  // Some existing pods were created without CLAW_GATEWAY_API_KEY injected.
21
21
  // Keep env-driven behavior first, but provide a dev-compatible fallback to avoid
22
22
  // breaking deliverable uploads during rolling migration.
23
23
  var API_KEY = process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY || "api-key-1";
24
+ var GATEWAY_PUBLIC = resolveGatewayPublicBase();
24
25
 
25
26
  var TOOL_DEFS = [
26
27
  {
@@ -93,6 +94,200 @@ function parseURL(rawURL) {
93
94
  }
94
95
  }
95
96
 
97
+ function isPrivateIPv4(hostname) {
98
+ if (!hostname) return false;
99
+ if (/^10\./.test(hostname)) return true;
100
+ if (/^127\./.test(hostname)) return true;
101
+ if (/^192\.168\./.test(hostname)) return true;
102
+ var m = hostname.match(/^172\.(\d+)\./);
103
+ if (m) {
104
+ var second = parseInt(m[1], 10);
105
+ return second >= 16 && second <= 31;
106
+ }
107
+ return false;
108
+ }
109
+
110
+ function isInternalHostname(hostname) {
111
+ var host = String(hostname || "").toLowerCase();
112
+ if (!host) return false;
113
+ if (host === "localhost" || host === "claw-gateway") return true;
114
+ if (host.indexOf(".svc") >= 0 || host.indexOf(".cluster.local") >= 0) return true;
115
+ if (host.indexOf("claw-gateway:") === 0) return true;
116
+ if (isPrivateIPv4(host)) return true;
117
+ if (host.indexOf(".") < 0) return true;
118
+ return false;
119
+ }
120
+
121
+ function isProbablyInternalURL(rawURL) {
122
+ var parsed = parseURL(rawURL);
123
+ if (!parsed) return false;
124
+ return isInternalHostname(parsed.hostname);
125
+ }
126
+
127
+ function envGatewayPublicFallback() {
128
+ var helmEnv = String(process.env.HELM_ENV || process.env.ENV || "").toLowerCase();
129
+ if (helmEnv === "prod" || helmEnv === "production" || helmEnv === "online") {
130
+ return "https://claw-gateway.csagentai.com";
131
+ }
132
+ if (helmEnv === "dev" || helmEnv === "development" || helmEnv === "staging" || helmEnv === "stage") {
133
+ return "https://claw-dev.csaiagent.com";
134
+ }
135
+ return "";
136
+ }
137
+
138
+ function resolveGatewayPublicBase() {
139
+ var candidate = RAW_GATEWAY_PUBLIC;
140
+ if (candidate && !isProbablyInternalURL(candidate)) {
141
+ return candidate;
142
+ }
143
+ var fallback = envGatewayPublicFallback();
144
+ if (fallback) {
145
+ return fallback.replace(/\/$/, "");
146
+ }
147
+ return candidate;
148
+ }
149
+
150
+ function absolutizePublicURL(rawURL) {
151
+ var val = String(rawURL || "").trim();
152
+ if (!val) return "";
153
+ if (/^https?:\/\//i.test(val)) {
154
+ if (isProbablyInternalURL(val) && GATEWAY_PUBLIC && !isProbablyInternalURL(GATEWAY_PUBLIC)) {
155
+ var parsed = parseURL(val);
156
+ if (parsed) {
157
+ return GATEWAY_PUBLIC + parsed.pathname + (parsed.search || "") + (parsed.hash || "");
158
+ }
159
+ }
160
+ return val;
161
+ }
162
+ if (val.charAt(0) !== "/") {
163
+ val = "/" + val;
164
+ }
165
+ return (GATEWAY_PUBLIC || RAW_GATEWAY_PUBLIC || "").replace(/\/$/, "") + val;
166
+ }
167
+
168
+ function outputPublicBase() {
169
+ return absolutizePublicURL("/openclaw-gateway/output");
170
+ }
171
+
172
+ function encodePathSegments(parts) {
173
+ return parts.map(function(part) {
174
+ return encodeURIComponent(String(part || ""));
175
+ }).join("/");
176
+ }
177
+
178
+ function sanitizePathSegment(v) {
179
+ var s = String(v || "").trim();
180
+ if (!s) return "unknown";
181
+ s = s.replace(/\//g, "_");
182
+ s = s.replace(/\\/g, "_");
183
+ s = s.replace(/\.\./g, "_");
184
+ return s;
185
+ }
186
+
187
+ function sanitizeFileName(v) {
188
+ var name = String(v || "").trim();
189
+ if (!name) return "file";
190
+ name = name.replace(/^.*[\/\\]/, "");
191
+ name = name.replace(/\//g, "_");
192
+ name = name.replace(/\\/g, "_");
193
+ name = name.replace(/\.\./g, "_");
194
+ if (!name || name === ".") return "file";
195
+ return name;
196
+ }
197
+
198
+ function extractUserIDFromResource(resourceID) {
199
+ var trimmed = String(resourceID || "").trim();
200
+ var prefix = "user_";
201
+ if (!trimmed || trimmed.indexOf(prefix) !== 0) {
202
+ return "";
203
+ }
204
+ var rest = trimmed.slice(prefix.length);
205
+ var end = rest.indexOf("_");
206
+ if (end <= 0) {
207
+ return "";
208
+ }
209
+ return rest.slice(0, end);
210
+ }
211
+
212
+ function deriveRelease(release, resourceID, userID) {
213
+ var rel = sanitizePathSegment(release);
214
+ if (rel && rel !== "unknown") {
215
+ return rel;
216
+ }
217
+ var uid = String(userID || "").trim();
218
+ if (!uid) {
219
+ uid = extractUserIDFromResource(resourceID);
220
+ }
221
+ if (uid) {
222
+ return "user-" + uid;
223
+ }
224
+ return "unknown";
225
+ }
226
+
227
+ function looksLikePreviewRoute(rawURL) {
228
+ var val = String(rawURL || "").trim();
229
+ return /\/openclaw-gateway\/preview\/[^/?#]+(?:[?#].*)?$/i.test(val);
230
+ }
231
+
232
+ function needsOutputURLRepair(data) {
233
+ if (!data) return false;
234
+ if (String(data.backend || "").toLowerCase() !== "output") {
235
+ return false;
236
+ }
237
+ var previewURL = String(data.previewUrl || "").trim();
238
+ var downloadURL = String(data.downloadUrl || "").trim();
239
+ if (!previewURL || !downloadURL) {
240
+ return true;
241
+ }
242
+ if (previewURL.charAt(0) === "/" || downloadURL.charAt(0) === "/") {
243
+ return true;
244
+ }
245
+ if (isProbablyInternalURL(previewURL) || isProbablyInternalURL(downloadURL)) {
246
+ return true;
247
+ }
248
+ if (looksLikePreviewRoute(previewURL) || looksLikePreviewRoute(downloadURL)) {
249
+ return true;
250
+ }
251
+ return false;
252
+ }
253
+
254
+ function fetchDeliverableMeta(uuid) {
255
+ if (!uuid) return Promise.resolve(null);
256
+ return httpRequest("GET", "/openclaw-gateway/be/deliverables/" + encodeURIComponent(uuid)).then(function(resp) {
257
+ return resp.body.data || resp.body || null;
258
+ });
259
+ }
260
+
261
+ function buildOutputURLs(meta, args, body, data) {
262
+ var base = outputPublicBase();
263
+ if (!base) {
264
+ return null;
265
+ }
266
+ var resourceID = sanitizePathSegment((meta && meta.resourceId) || body.resourceId || args.resource_id);
267
+ var userID = (meta && meta.userId) || body.userId || args.user_id;
268
+ var release = deriveRelease((meta && meta.release) || body.release, resourceID, userID);
269
+ var uuid = String((meta && meta.uuid) || (data && data.uuid) || "").trim();
270
+ if (!resourceID || !uuid) {
271
+ return null;
272
+ }
273
+ var isDirectory = !!(args.files && args.files.length > 0) || String((meta && meta.mime) || "").trim() === "application/x-directory";
274
+ if (isDirectory) {
275
+ var dirURL = base.replace(/\/$/, "") + "/" + encodePathSegments([release, resourceID, uuid]);
276
+ return {
277
+ previewURL: dirURL,
278
+ downloadURL: dirURL + "?list=1",
279
+ isDirectory: true
280
+ };
281
+ }
282
+ var fileName = sanitizeFileName((meta && meta.fileName) || body.fileName || args.file_name);
283
+ var fileURL = base.replace(/\/$/, "") + "/" + encodePathSegments([release, resourceID, uuid, fileName]);
284
+ return {
285
+ previewURL: fileURL,
286
+ downloadURL: fileURL,
287
+ isDirectory: false
288
+ };
289
+ }
290
+
96
291
  function httpRequest(method, path, body) {
97
292
  return new Promise(function(resolve, reject) {
98
293
  var parsed = parseURL(GATEWAY_URL + path);
@@ -167,6 +362,17 @@ function buildReplyMarkdown(opts) {
167
362
  return lines.join("\n");
168
363
  }
169
364
 
365
+ function resolveReplyURLs(data, args, body) {
366
+ if (!needsOutputURLRepair(data)) {
367
+ return Promise.resolve(null);
368
+ }
369
+ return fetchDeliverableMeta(data.uuid).catch(function() {
370
+ return null;
371
+ }).then(function(meta) {
372
+ return buildOutputURLs(meta, args, body, data);
373
+ });
374
+ }
375
+
170
376
  // ─── Tool implementations ─────────────────────────────────────────────────────
171
377
 
172
378
  function uploadDeliverable(args) {
@@ -205,29 +411,32 @@ function uploadDeliverable(args) {
205
411
 
206
412
  return httpRequest("POST", "/openclaw-gateway/be/deliverables", body).then(function(resp) {
207
413
  var d = resp.body.data || resp.body;
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
414
+ return resolveReplyURLs(d, args, body).then(function(repaired) {
415
+ // Prefer backend-aware previewUrl returned by gateway (OSS/output/link).
416
+ // Fallback to legacy gateway preview endpoint for compatibility.
417
+ var previewURL = absolutizePublicURL((repaired && repaired.previewURL) || d.previewUrl || ("/openclaw-gateway/preview/" + d.uuid));
418
+ var downloadURL = absolutizePublicURL((repaired && repaired.downloadURL) || d.downloadUrl);
419
+ var isDirectory = !!(repaired && repaired.isDirectory) || !!(args.files && args.files.length > 0);
420
+ if (!isDirectory && downloadURL && previewURL && downloadURL !== previewURL && /(?:\?|&)list=1(?:&|$)/.test(downloadURL)) {
421
+ isDirectory = true;
422
+ }
423
+ var replyMarkdown = buildReplyMarkdown({
424
+ previewURL: previewURL,
425
+ downloadURL: downloadURL,
426
+ type: args.type,
427
+ isDirectory: isDirectory
428
+ });
429
+ return {
430
+ uuid: d.uuid,
431
+ backend: d.backend || "",
432
+ download_url: downloadURL,
433
+ preview_url: previewURL,
434
+ expire_at: d.expireAt,
435
+ reply_markdown: replyMarkdown,
436
+ trace_id: resp.traceID || "",
437
+ message: replyMarkdown
438
+ };
220
439
  });
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
- };
231
440
  });
232
441
  }
233
442
 
@@ -252,7 +461,7 @@ function handleMessage(msg) {
252
461
  send({ jsonrpc: "2.0", id: id, result: {
253
462
  protocolVersion: "2024-11-05",
254
463
  capabilities: { tools: {} },
255
- serverInfo: { name: "deliverables", version: "1.0.2" }
464
+ serverInfo: { name: "deliverables", version: "1.0.9" }
256
465
  }});
257
466
  return Promise.resolve();
258
467
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
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": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dai_ming/plugin-deliverables",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
4
4
  "description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
5
5
  "keywords": [
6
6
  "openclaw",
@@ -57,13 +57,19 @@ files: [
57
57
  ## 上传成功后
58
58
 
59
59
  优先规则:
60
- - 最终消息必须先用你自己的话写 1-2 句简短介绍,粗略说明你生成了什么内容、包含哪些重点。
61
- - 这句介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
62
- - 如果工具结果里有 `reply_markdown`,把它原样放在这句介绍后面,不要改写其中的链接。
60
+ - 最终消息必须先给出一段更完整的 Markdown 介绍,不要只写一句短确认。
61
+ - 建议结构是:
62
+ - 一行简短标题或总结
63
+ - `### 内容概览`
64
+ - 3-6 个 bullet,概括这份产物实际包含的章节、重点、亮点或功能
65
+ - 这段介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
66
+ - 对于常见文章、介绍、报告类交付物,链接前的介绍通常应达到约 80-200 个中文字符,明显长于一句话确认。
67
+ - bullet 必须写具体内容,例如“基本信息、代表作品、奖项与影响力”,不要只写“内容完整、结构清晰”这类空话。
68
+ - 如果工具结果里有 `reply_markdown`,先输出 `### 访问链接`,再把它原样放在后面,不要改写其中的链接。
63
69
 
64
70
  否则只发给用户:
65
71
  - 预览链接(`preview_url`,必须用 Markdown 链接格式)
66
72
  - 下载链接(`download_url`,必须用 Markdown 链接格式)
67
73
  - 多文件/游戏目录场景下,第二行也可以是 `文件列表:[查看目录](...)`,不要改成裸 URL 或 zip 描述。
68
74
 
69
- 不要在消息里输出文件的完整内容、工作区保存路径或裸链接。
75
+ 不要在消息里输出文件的完整内容、工作区保存路径或裸链接,也不要只输出“一句简介 + 两个链接”这种过短格式。