@dai_ming/plugin-deliverables 1.0.7 → 1.0.10
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 +13 -2
- package/agents-rules/deliverables.md +38 -28
- package/mcp-servers/deliverables.js +308 -26
- package/openclaw-plugin.json +1 -1
- package/package.json +1 -1
- package/skills/deliverables/SKILL.md +18 -10
package/README.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则和 `openclaw.json` 配置一次性落到运行目录里,让 Agent 默认把生成文件上传成可访问的交付物链接。
|
|
4
4
|
|
|
5
|
+
当前版本增强点:
|
|
6
|
+
|
|
7
|
+
- 上传成功后优先走“两条回复”体验:
|
|
8
|
+
- 第一条是内容摘要
|
|
9
|
+
- 第二条是纯 Markdown 链接
|
|
10
|
+
- 单文件交付物会尽量保留原始文件后缀;如果模型漏掉后缀,插件会按内容类型自动补齐(如 `.md` / `.html`)
|
|
11
|
+
|
|
5
12
|
## 包内文件
|
|
6
13
|
|
|
7
14
|
| 文件 | 作用 |
|
|
@@ -20,7 +27,7 @@ OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则
|
|
|
20
27
|
|
|
21
28
|
```yaml
|
|
22
29
|
installPlugins:
|
|
23
|
-
- "@dai_ming/plugin-deliverables@1.0.
|
|
30
|
+
- "@dai_ming/plugin-deliverables@1.0.10"
|
|
24
31
|
```
|
|
25
32
|
|
|
26
33
|
现有 chart 会在 init 阶段完成安装。
|
|
@@ -31,7 +38,7 @@ installPlugins:
|
|
|
31
38
|
|
|
32
39
|
```bash
|
|
33
40
|
npm config set registry https://registry.npmmirror.com
|
|
34
|
-
npm install @dai_ming/plugin-deliverables@1.0.
|
|
41
|
+
npm install @dai_ming/plugin-deliverables@1.0.10
|
|
35
42
|
node node_modules/@dai_ming/plugin-deliverables/install.js
|
|
36
43
|
```
|
|
37
44
|
|
|
@@ -133,3 +140,7 @@ env | grep -E 'CLAW_GATEWAY|OPENCLAW_GATEWAY|botID'
|
|
|
133
140
|
```bash
|
|
134
141
|
curl -H "X-API-Key: $CLAW_GATEWAY_API_KEY" "$CLAW_GATEWAY_URL/healthz"
|
|
135
142
|
```
|
|
143
|
+
|
|
144
|
+
### 返回的是 `/preview/:uuid` 或内部地址
|
|
145
|
+
|
|
146
|
+
`1.0.10` 起,插件会在 `output` 交付物场景下自动把相对/内部 preview 地址修正为最终的外部 `output` 直链,不再依赖 gateway 当前环境里是否显式配置了 `deliverable.gatewayPublicUrl`;同时会尽量保留原始文件后缀,并把成功后的用户回复拆成“摘要 + 纯链接”两段式。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<!-- DELIVERABLE_LINK_RULES_START -->
|
|
2
|
-
## Deliverables --
|
|
2
|
+
## Deliverables -- Two Reply Link Rule (HARD CONSTRAINT)
|
|
3
3
|
|
|
4
|
-
When you call the deliverables upload tool,
|
|
4
|
+
When you call the deliverables upload tool, the user-facing output MUST make the deliverable easy to open and easy to understand.
|
|
5
5
|
|
|
6
6
|
Tool name note:
|
|
7
7
|
|
|
@@ -12,32 +12,36 @@ Tool name note:
|
|
|
12
12
|
|
|
13
13
|
### Required output behavior
|
|
14
14
|
|
|
15
|
-
0.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- a
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
6
|
|
15
|
+
0. If the current session exposes a tool literally named `message`, you MUST send **two visible replies** after a successful upload:
|
|
16
|
+
- first reply: use the `message` tool to send a pure content summary only
|
|
17
|
+
- second reply: use the normal final assistant reply to send pure links only
|
|
18
|
+
1. The first reply (via `message` tool) MUST:
|
|
19
|
+
- be based on the actual content/request, not a fixed boilerplate
|
|
20
|
+
- contain no links, no raw URL, no workspace path
|
|
21
|
+
- use Markdown structure, preferably:
|
|
22
|
+
- a short title or summary line
|
|
23
|
+
- `### 内容概览`
|
|
24
|
+
- 3-6 bullet points describing the real sections, highlights, features, or focus
|
|
25
|
+
- be noticeably more informative than one sentence; for normal articles/reports/introductions, aim for roughly 80-220 Chinese characters total
|
|
26
|
+
2. The second reply (final assistant reply) MUST contain links only, with no extra intro, no trailing explanation, no workspace path, and no naked long URL.
|
|
27
|
+
3. If tool result contains `reply_markdown`, the second reply MUST be exactly that link block verbatim. Do not rewrite the links inside it.
|
|
28
|
+
4. If tool result has `preview_url`, include one line:
|
|
25
29
|
`预览链接:[点击预览](<full_url>)`
|
|
26
|
-
|
|
30
|
+
5. If tool result has `download_url`, include one line:
|
|
27
31
|
`下载链接:[点击下载](<full_url>)`
|
|
28
|
-
|
|
32
|
+
6. For multi-file/game deliverables, if tool result already formats the second line as
|
|
29
33
|
`文件列表:[查看目录](<full_url>)`
|
|
30
34
|
then keep that exact label and URL. Do not rewrite it back to a raw long link.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
|
35
|
+
7. Keep URL target value exactly from tool result (no shortening, no masking, no redirect rewrite).
|
|
36
|
+
8. Use short link labels (`点击预览` / `点击下载` / `查看目录`) to avoid exposing long raw URLs.
|
|
37
|
+
9. Do not stop after the first `message` call; you still owe the second visible links reply.
|
|
38
|
+
10. If the current session does **not** expose a `message` tool, fall back to one Markdown final reply: content summary first, then the link block.
|
|
36
39
|
|
|
37
40
|
### Forbidden output behavior
|
|
38
41
|
|
|
39
42
|
- "可点击预览链接查看" but no actual URL
|
|
40
|
-
-
|
|
43
|
+
- Putting links into the first summary reply
|
|
44
|
+
- Putting extra narrative text into the second pure-links reply
|
|
41
45
|
- Replacing URL target with a non-original URL
|
|
42
46
|
- Omitting both links when tool already returned links
|
|
43
47
|
- Outputting only naked long URL without Markdown link label
|
|
@@ -61,20 +65,26 @@ Tool name note:
|
|
|
61
65
|
1. Create content under current agent workspace `output/` directory first (for example `output/report.md`), then call the deliverables upload tool exposed in this session.
|
|
62
66
|
2. If you need to use the `write` tool, the target path MUST be inside `output/`. Never write deliverable files to the workspace root.
|
|
63
67
|
3. If you already wrote the file to the wrong place, move/copy it into `output/` before finalizing the task and before describing the saved path to the user.
|
|
64
|
-
4.
|
|
65
|
-
|
|
68
|
+
4. The deliverable file name MUST preserve the intended extension exactly. Examples:
|
|
69
|
+
- Markdown article: `刘德华介绍.md`
|
|
70
|
+
- Markdown note: `周杰伦.md`
|
|
71
|
+
- HTML page: `996专题.html`
|
|
72
|
+
- Text file: `旅行清单.txt`
|
|
73
|
+
5. Never drop the suffix from the final deliverable file name. If the user asked for Markdown, keep `.md`; if HTML, keep `.html`.
|
|
66
74
|
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.
|
|
67
75
|
7. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
|
|
68
76
|
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.
|
|
69
|
-
9. After successful upload,
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
9. After successful upload, if `message` tool is available, you MUST:
|
|
78
|
+
- first send a richer Markdown content summary through `message`
|
|
79
|
+
- then send a second pure-links final reply
|
|
80
|
+
10. The summary must be based on the actual deliverable content/request, not a fixed sentence like `交付物已上传成功,可直接在线预览或下载。`
|
|
81
|
+
11. Prefer this Markdown structure for the first summary reply:
|
|
72
82
|
- a short title or summary line
|
|
73
83
|
- `### 内容概览`
|
|
74
84
|
- 3-6 bullet points summarizing the actual content sections, highlights, or key features
|
|
75
|
-
12. For ordinary articles/reports/introductions, the
|
|
76
|
-
13. If tool result contains `reply_markdown`,
|
|
77
|
-
14. Otherwise
|
|
85
|
+
12. For ordinary articles/reports/introductions, the summary should normally reach roughly 80-220 Chinese characters total.
|
|
86
|
+
13. If tool result contains `reply_markdown`, the second reply MUST output that field verbatim and nothing else.
|
|
87
|
+
14. Otherwise the second reply MUST include only Markdown links (short label + full URL target):
|
|
78
88
|
`预览链接:[点击预览](<full_url>)`
|
|
79
89
|
`下载链接:[点击下载](<full_url>)`
|
|
80
90
|
15. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
|
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
var http = require("http");
|
|
16
16
|
var https = require("https");
|
|
17
|
+
var path = require("path");
|
|
17
18
|
|
|
18
19
|
var GATEWAY_URL = (process.env.CLAW_GATEWAY_URL || "http://claw-gateway:8080").replace(/\/$/, "");
|
|
19
|
-
var
|
|
20
|
+
var RAW_GATEWAY_PUBLIC = (process.env.CLAW_GATEWAY_PUBLIC_URL || GATEWAY_URL).replace(/\/$/, "");
|
|
20
21
|
// Some existing pods were created without CLAW_GATEWAY_API_KEY injected.
|
|
21
22
|
// Keep env-driven behavior first, but provide a dev-compatible fallback to avoid
|
|
22
23
|
// breaking deliverable uploads during rolling migration.
|
|
23
24
|
var API_KEY = process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY || "api-key-1";
|
|
25
|
+
var GATEWAY_PUBLIC = resolveGatewayPublicBase();
|
|
24
26
|
|
|
25
27
|
var TOOL_DEFS = [
|
|
26
28
|
{
|
|
@@ -54,7 +56,7 @@ var TOOL_DEFS = [
|
|
|
54
56
|
},
|
|
55
57
|
file_name: {
|
|
56
58
|
type: "string",
|
|
57
|
-
description: "
|
|
59
|
+
description: "用户可见的文件名,必须尽量保留原始扩展名,例如 刘德华介绍.md、report.html。多文件交付时这里应是目录名/项目名,不要写成 .zip,除非用户明确要求压缩包。"
|
|
58
60
|
},
|
|
59
61
|
content_text: {
|
|
60
62
|
type: "string",
|
|
@@ -93,6 +95,269 @@ function parseURL(rawURL) {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
|
|
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
|
+
|
|
96
361
|
function httpRequest(method, path, body) {
|
|
97
362
|
return new Promise(function(resolve, reject) {
|
|
98
363
|
var parsed = parseURL(GATEWAY_URL + path);
|
|
@@ -167,15 +432,27 @@ function buildReplyMarkdown(opts) {
|
|
|
167
432
|
return lines.join("\n");
|
|
168
433
|
}
|
|
169
434
|
|
|
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
|
+
|
|
170
446
|
// ─── Tool implementations ─────────────────────────────────────────────────────
|
|
171
447
|
|
|
172
448
|
function uploadDeliverable(args) {
|
|
449
|
+
var finalFileName = normalizeDeliverableFileName(args.file_name, args.type, args.content_text, args.files);
|
|
173
450
|
var body = {
|
|
174
451
|
resourceId: args.resource_id,
|
|
175
452
|
groupId: args.group_id,
|
|
176
453
|
userId: args.user_id,
|
|
177
454
|
type: args.type,
|
|
178
|
-
fileName:
|
|
455
|
+
fileName: finalFileName,
|
|
179
456
|
release: "" // overwritten below after release name derivation
|
|
180
457
|
};
|
|
181
458
|
|
|
@@ -205,29 +482,34 @@ function uploadDeliverable(args) {
|
|
|
205
482
|
|
|
206
483
|
return httpRequest("POST", "/openclaw-gateway/be/deliverables", body).then(function(resp) {
|
|
207
484
|
var d = resp.body.data || resp.body;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
isDirectory =
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
485
|
+
return resolveReplyURLs(d, args, body).then(function(repaired) {
|
|
486
|
+
// Prefer backend-aware previewUrl returned by gateway (OSS/output/link).
|
|
487
|
+
// Fallback to legacy gateway preview endpoint for compatibility.
|
|
488
|
+
var previewURL = absolutizePublicURL((repaired && repaired.previewURL) || d.previewUrl || ("/openclaw-gateway/preview/" + d.uuid));
|
|
489
|
+
var downloadURL = absolutizePublicURL((repaired && repaired.downloadURL) || d.downloadUrl);
|
|
490
|
+
var isDirectory = !!(repaired && repaired.isDirectory) || !!(args.files && args.files.length > 0);
|
|
491
|
+
if (!isDirectory && downloadURL && previewURL && downloadURL !== previewURL && /(?:\?|&)list=1(?:&|$)/.test(downloadURL)) {
|
|
492
|
+
isDirectory = true;
|
|
493
|
+
}
|
|
494
|
+
var replyMarkdown = buildReplyMarkdown({
|
|
495
|
+
previewURL: previewURL,
|
|
496
|
+
downloadURL: downloadURL,
|
|
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
|
+
};
|
|
220
512
|
});
|
|
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
513
|
});
|
|
232
514
|
}
|
|
233
515
|
|
|
@@ -252,7 +534,7 @@ function handleMessage(msg) {
|
|
|
252
534
|
send({ jsonrpc: "2.0", id: id, result: {
|
|
253
535
|
protocolVersion: "2024-11-05",
|
|
254
536
|
capabilities: { tools: {} },
|
|
255
|
-
serverInfo: { name: "deliverables", version: "1.0.
|
|
537
|
+
serverInfo: { name: "deliverables", version: "1.0.10" }
|
|
256
538
|
}});
|
|
257
539
|
return Promise.resolve();
|
|
258
540
|
|
package/openclaw-plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -21,6 +21,8 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
21
21
|
- 多文件内容(游戏/网站)写到 `output/<目录名>/...`,保持目录结构。
|
|
22
22
|
- 如果你使用 `write` 工具创建交付物文件,目标路径必须在 `output/` 下。
|
|
23
23
|
- 如果你已经误写到工作区根目录,必须先移动或复制到 `output/`,再继续上传交付物并向用户汇报。
|
|
24
|
+
- 交付物文件名必须保留原始扩展名,不要丢后缀。示例:`刘德华介绍.md`、`周杰伦.md`、`996专题.html`。
|
|
25
|
+
- 如果用户要求 Markdown,就保留 `.md`;如果用户要求 HTML,就保留 `.html`;如果是纯文本,就保留 `.txt`。
|
|
24
26
|
|
|
25
27
|
## 调用前必须准备的参数(全部必填)
|
|
26
28
|
|
|
@@ -32,7 +34,7 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
32
34
|
| `group_id` | 消息元数据中的 `group_id` 或 `conversation_id` | `group_abc123` |
|
|
33
35
|
| `user_id` | 消息元数据中的 `sender_id` 或 `owner_id` | `cbb0fab9...` |
|
|
34
36
|
| `type` | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`zip`/`link` | `article` |
|
|
35
|
-
| `file_name` |
|
|
37
|
+
| `file_name` | 有意义的文件名,必须含正确扩展名 | `report-2026.html` |
|
|
36
38
|
| `content_text` | 文件的完整文本内容(HTML/Markdown等) | `<html>...</html>` |
|
|
37
39
|
|
|
38
40
|
> **直接对话(direct chat)时**:`group_id` 填 `conversation_id`,`user_id` 填 `owner_id`。
|
|
@@ -57,19 +59,25 @@ files: [
|
|
|
57
59
|
## 上传成功后
|
|
58
60
|
|
|
59
61
|
优先规则:
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
+
- 如果当前会话里存在一个名字就叫 `message` 的工具,上传成功后必须发两条可见回复:
|
|
63
|
+
- 第一条:通过 `message` 工具发送,只写内容介绍,不放链接
|
|
64
|
+
- 第二条:正常最终回复里只放链接,不加额外介绍
|
|
65
|
+
- 第一条内容介绍建议结构是:
|
|
62
66
|
- 一行简短标题或总结
|
|
63
67
|
- `### 内容概览`
|
|
64
68
|
- 3-6 个 bullet,概括这份产物实际包含的章节、重点、亮点或功能
|
|
65
69
|
- 这段介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
|
|
66
|
-
-
|
|
70
|
+
- 对于常见文章、介绍、报告类交付物,第一条介绍通常应达到约 80-220 个中文字符,明显长于一句话确认。
|
|
67
71
|
- bullet 必须写具体内容,例如“基本信息、代表作品、奖项与影响力”,不要只写“内容完整、结构清晰”这类空话。
|
|
68
|
-
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
72
|
+
- 第一条介绍里不要放工作区路径、裸链接、Markdown 链接或下载提示。
|
|
73
|
+
- 第二条链接回复里,如果工具结果有 `reply_markdown`,直接原样输出它,不要改写,不要再加标题或说明。
|
|
74
|
+
- 第二条链接回复只允许包含:
|
|
75
|
+
- `预览链接:[点击预览](...)`
|
|
76
|
+
- `下载链接:[点击下载](...)`
|
|
77
|
+
- 或目录场景下的 `文件列表:[查看目录](...)`
|
|
78
|
+
- 不要把第一条和第二条混在一起,也不要在第二条链接消息里再补“已完成”“请查看”等多余文字。
|
|
79
|
+
|
|
80
|
+
回退规则:
|
|
81
|
+
- 如果当前会话没有 `message` 工具,再退回成一条最终 Markdown 回复:先介绍,再附链接。
|
|
74
82
|
|
|
75
83
|
不要在消息里输出文件的完整内容、工作区保存路径或裸链接,也不要只输出“一句简介 + 两个链接”这种过短格式。
|