@dai_ming/plugin-deliverables 1.0.9 → 1.0.14

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
@@ -2,15 +2,21 @@
2
2
 
3
3
  OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则和 `openclaw.json` 配置一次性落到运行目录里,让 Agent 默认把生成文件上传成可访问的交付物链接。
4
4
 
5
+ 当前版本增强点:
6
+
7
+ - 上传成功后返回标准 Markdown 链接块;同时通过运行时补丁把 palz-connector 中的“简介 + 链接”交付物回复拆成两条独立消息
8
+ - 单文件交付物会尽量保留原始文件后缀;如果模型漏掉后缀,插件会按内容类型自动补齐(如 `.md` / `.html`)
9
+
5
10
  ## 包内文件
6
11
 
7
12
  | 文件 | 作用 |
8
13
  |------|------|
9
- | `install.js` | 安装器:负责复制插件、注册 MCP、安装 skill、注入 AGENTS 规则、补齐 `plugins.allow`、清 session |
14
+ | `install.js` | 安装器:负责复制插件、注册 MCP、安装 skill、注入 AGENTS 规则、补齐 `plugins.allow`、清 session,并刷新旧的 deliverables MCP 进程 |
10
15
  | `mcp-servers/deliverables.js` | MCP Server,暴露 `upload_deliverable` |
11
16
  | `skills/deliverables/SKILL.md` | 约束模型优先走交付物上传,并统一写到 `output/` |
12
17
  | `agents-rules/deliverables.md` | 注入到 `AGENTS.md` 的强约束规则 |
13
- | `openclaw-plugin.json` | 插件清单,声明 MCP、skill、rules 和运行时配置 |
18
+ | `openclaw-plugin.json` | 兼容当前安装脚本的清单,声明 MCP、skill、rules 和运行时配置 |
19
+ | `openclaw.plugin.json` | OpenClaw 运行时插件清单,用于真正加载 `index.js` 扩展 |
14
20
 
15
21
  ## 推荐安装方式
16
22
 
@@ -20,7 +26,7 @@ OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则
20
26
 
21
27
  ```yaml
22
28
  installPlugins:
23
- - "@dai_ming/plugin-deliverables@1.0.9"
29
+ - "@dai_ming/plugin-deliverables@1.0.14"
24
30
  ```
25
31
 
26
32
  现有 chart 会在 init 阶段完成安装。
@@ -31,7 +37,7 @@ installPlugins:
31
37
 
32
38
  ```bash
33
39
  npm config set registry https://registry.npmmirror.com
34
- npm install @dai_ming/plugin-deliverables@1.0.9
40
+ npm install @dai_ming/plugin-deliverables@1.0.14
35
41
  node node_modules/@dai_ming/plugin-deliverables/install.js
36
42
  ```
37
43
 
@@ -57,9 +63,16 @@ node node_modules/@dai_ming/plugin-deliverables/install.js \
57
63
  - 注册 `mcp.servers.deliverables`
58
64
  - 启用 `skills.entries.deliverables`
59
65
  - 启用 `plugins.entries.plugin-deliverables`
66
+ - 把 `~/.openclaw/extensions-extra/plugin-deliverables` 写入 `plugins.load.paths`
60
67
  - 把 `plugin-deliverables` 加入 `plugins.allow`
61
68
  7. 清理 session,让下一条消息重新读取 prompt / skill / tool 配置
62
69
  8. 如果 agent 或全局存在 `tools.allow` 白名单,自动补上 `deliverables__upload_deliverable`
70
+ 9. 如果当前 pod 里已经跑着旧版 `deliverables.js` MCP 进程,会自动结束旧进程,让它按新文件重新拉起,避免“磁盘已更新但 toolResult 还是旧格式”
71
+
72
+ 补充说明:
73
+
74
+ - 当前 OpenClaw 运行时默认只扫描 `~/.openclaw/extensions`;`extensions-extra` 里的 JS 插件只有进入 `plugins.load.paths` 才会被当成可执行 `openclaw` 插件加载
75
+ - 安装脚本会先把插件源码暂存到系统临时目录再复制,避免在 `extensions-extra` 原地执行安装时把源目录自己删掉
63
76
 
64
77
  ## 是否需要重启 Pod
65
78
 
@@ -67,7 +80,7 @@ node node_modules/@dai_ming/plugin-deliverables/install.js \
67
80
 
68
81
  原因:
69
82
  - 安装脚本会直接改 `~/.openclaw/openclaw.json`
70
- - `plugins.allow` 变化会触发 OpenClaw 的 watcher,进程级重载大约 15 秒
83
+ - `plugins.allow` / `plugins.load.paths` 变化会触发 OpenClaw 的 watcher,进程级重载大约 15 秒
71
84
  - session 也会被清掉,所以下一条消息会重新读取最新的 `AGENTS.md` / skill
72
85
 
73
86
  建议做法:
@@ -136,4 +149,4 @@ curl -H "X-API-Key: $CLAW_GATEWAY_API_KEY" "$CLAW_GATEWAY_URL/healthz"
136
149
 
137
150
  ### 返回的是 `/preview/:uuid` 或内部地址
138
151
 
139
- `1.0.9` 起,插件会在 `output` 交付物场景下自动把相对/内部 preview 地址修正为最终的外部 `output` 直链,不再依赖 gateway 当前环境里是否显式配置了 `deliverable.gatewayPublicUrl`。
152
+ `1.0.14` 起,安装脚本会在落盘新的 `deliverables.js` 后自动刷新正在运行的旧 MCP 进程,避免出现“插件目录和 `extensions-extra` 都是新代码,但 `~/.openclaw/mcp-servers/deliverables.js` 对应的常驻进程仍返回旧 `reply_markdown`”的问题。此前 `1.0.13` 已经完成 `output` 直链修正、后缀保留,以及 palz-connector 中“内容简介 + 链接”自动拆成两条消息,第二条只保留一个最终 HTTPS 链接。
@@ -1,7 +1,7 @@
1
1
  <!-- DELIVERABLE_LINK_RULES_START -->
2
2
  ## Deliverables -- URL Echo Rule (HARD CONSTRAINT)
3
3
 
4
- When you call the deliverables upload tool, your final assistant message MUST include clickable URLs in Markdown link format (short label + full URL target).
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
 
@@ -20,28 +20,20 @@ Tool name note:
20
20
  - 3-6 bullet points describing the actual sections, highlights, features, or focus of the deliverable
21
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
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:
25
- `预览链接:[点击预览](<full_url>)`
26
- 7. If tool result has `download_url`, include one line:
27
- `下载链接:[点击下载](<full_url>)`
28
- 8. For multi-file/game deliverables, if tool result already formats the second line as
29
- `文件列表:[查看目录](<full_url>)`
30
- then keep that exact label and URL. Do not rewrite it back to a raw long link.
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.
23
+ 5. If tool result contains `reply_markdown`, append a `### 访问链接` heading and then append that field verbatim on the following line. Do not rewrite the URL inside it.
24
+ 6. Otherwise, if tool result has `primary_url`, output only that one full URL on its own line after `### 访问链接`.
25
+ 7. If `primary_url` is missing, fall back to exactly one link: prefer `preview_url`, otherwise `download_url`.
26
+ 8. Keep URL target value exactly from tool result (no shortening, no masking, no redirect rewrite).
27
+ 9. Only output one final deliverable URL. Do not output both preview and download.
28
+ 10. Keep the intro concise but substantive; do not paste the full file content, saved-path text, or a huge essay after the URL.
36
29
 
37
30
  ### Forbidden output behavior
38
31
 
39
32
  - "可点击预览链接查看" but no actual URL
40
33
  - A single short sentence plus links when the deliverable is an article/report/introduction and concrete highlights are available
41
34
  - Replacing URL target with a non-original URL
42
- - Omitting both links when tool already returned links
43
- - Outputting only naked long URL without Markdown link label
44
- - Replacing `文件列表:[查看目录](...)` with a naked URL or a zip description
35
+ - Omitting the final URL when the tool already returned one
36
+ - Outputting both preview and download when one shareable URL is enough
45
37
  - Using a generic boilerplate intro that does not mention the actual deliverable content
46
38
  <!-- DELIVERABLE_LINK_RULES_END -->
47
39
 
@@ -61,8 +53,12 @@ Tool name note:
61
53
  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
54
  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
55
  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. If format is HTML, prefer `type=article` and file name ends with `.html`.
65
- 5. If format is markdown/text, use `type=article` and `.md`/`.txt`.
56
+ 4. The deliverable file name MUST preserve the intended extension exactly. Examples:
57
+ - Markdown article: `刘德华介绍.md`
58
+ - Markdown note: `周杰伦.md`
59
+ - HTML page: `996专题.html`
60
+ - Text file: `旅行清单.txt`
61
+ 5. Never drop the suffix from the final deliverable file name. If the user asked for Markdown, keep `.md`; if HTML, keep `.html`.
66
62
  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
63
  7. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
68
64
  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.
@@ -73,15 +69,11 @@ Tool name note:
73
69
  - `### 内容概览`
74
70
  - 3-6 bullet points summarizing the actual content sections, highlights, or key features
75
71
  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):
78
- `预览链接:[点击预览](<full_url>)`
79
- `下载链接:[点击下载](<full_url>)`
80
- 15. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
81
- `文件列表:[查看目录](<full_url>)`
82
- Keep that format instead of forcing a zip link.
72
+ 13. If tool result contains `reply_markdown`, add `### 访问链接` and append that field verbatim on the following line.
73
+ 14. Otherwise final reply MUST include exactly one full URL after `### 访问链接`: prefer `primary_url`, otherwise `preview_url`, otherwise `download_url`.
74
+ 15. Do NOT output preview/download two条链接,也不要把目录链接再包装成 zip 说明。
83
75
  16. Do NOT only say "已保存到工作空间".
84
- 17. Do NOT append workspace path or a raw URL block after the Markdown links.
76
+ 17. Do NOT append workspace path or extra raw URL blocks after the final URL.
85
77
 
86
78
  ### Exception
87
79
 
package/index.js ADDED
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ var FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
5
+
6
+ function isString(value) {
7
+ return typeof value === "string";
8
+ }
9
+
10
+ function trimTrailingBlankLines(lines) {
11
+ var out = lines.slice();
12
+ while (out.length > 0 && /^\s*$/.test(out[out.length - 1])) {
13
+ out.pop();
14
+ }
15
+ return out;
16
+ }
17
+
18
+ function stripBulletPrefix(line) {
19
+ return String(line || "").replace(/^\s*[-*+]\s+/, "");
20
+ }
21
+
22
+ function isLinksHeading(line) {
23
+ return /^\s*#{1,6}\s*访问链接\s*$/u.test(String(line || ""));
24
+ }
25
+
26
+ function extractDeliverableURL(line) {
27
+ var text = String(line || "").trim();
28
+ if (!text) {
29
+ return "";
30
+ }
31
+ var markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
32
+ if (markdownLink && markdownLink[1]) {
33
+ return markdownLink[1].trim();
34
+ }
35
+ var rawLink = text.match(/https?:\/\/\S+/iu);
36
+ if (rawLink && rawLink[0]) {
37
+ return rawLink[0].trim();
38
+ }
39
+ return "";
40
+ }
41
+
42
+ function isDeliverableLinkLine(line) {
43
+ var text = String(line || "");
44
+ if (/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(text)) {
45
+ return true;
46
+ }
47
+ return !!extractDeliverableURL(text);
48
+ }
49
+
50
+ function splitDeliverableMessage(text) {
51
+ var normalized = String(text || "").replace(/\r\n/g, "\n").trim();
52
+ if (!normalized) {
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();
72
+ }
73
+ summaryLines = trimTrailingBlankLines(summaryLines);
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;
93
+ }
94
+
95
+ return {
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;
109
+ }
110
+ return next;
111
+ }
112
+
113
+ function shouldSplitPalzPayload(body) {
114
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
115
+ return null;
116
+ }
117
+ if (!isString(body.content)) {
118
+ return null;
119
+ }
120
+ if (body.stream_id || body.seq !== undefined || body.delta !== undefined || body.is_final !== undefined) {
121
+ return null;
122
+ }
123
+ if (body.palz_msg_type || body.tool_content) {
124
+ return null;
125
+ }
126
+ return splitDeliverableMessage(body.content);
127
+ }
128
+
129
+ function resolveFetchURL(input) {
130
+ if (isString(input)) {
131
+ return input;
132
+ }
133
+ if (input && isString(input.url)) {
134
+ return input.url;
135
+ }
136
+ return "";
137
+ }
138
+
139
+ function resolveFetchMethod(input, init) {
140
+ if (init && isString(init.method) && init.method.trim()) {
141
+ return init.method.trim().toUpperCase();
142
+ }
143
+ if (input && !isString(input) && isString(input.method) && input.method.trim()) {
144
+ return input.method.trim().toUpperCase();
145
+ }
146
+ return "GET";
147
+ }
148
+
149
+ function installPalzFetchPatch(api) {
150
+ if (globalThis[FETCH_PATCH_KEY] && globalThis[FETCH_PATCH_KEY].installed) {
151
+ return;
152
+ }
153
+ var originalFetch = globalThis.fetch;
154
+ if (typeof originalFetch !== "function") {
155
+ api.logger.warn && api.logger.warn("[plugin-deliverables] global fetch is unavailable; split patch skipped");
156
+ return;
157
+ }
158
+
159
+ var patchedFetch = async function(input, init) {
160
+ var url = resolveFetchURL(input);
161
+ var method = resolveFetchMethod(input, init);
162
+ if (method !== "POST" || !/\/bot\/send(?:\?|$)/.test(url)) {
163
+ return originalFetch(input, init);
164
+ }
165
+
166
+ var requestBody = init && init.body;
167
+ if (!isString(requestBody)) {
168
+ return originalFetch(input, init);
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 || ""),
187
+ );
188
+
189
+ var baseInit = Object.assign({}, init);
190
+ var summaryInit = Object.assign({}, baseInit, {
191
+ body: JSON.stringify(cloneBody(parsed, split.summary, "__summary")),
192
+ });
193
+ var linksInit = Object.assign({}, baseInit, {
194
+ body: JSON.stringify(cloneBody(parsed, split.links, "__links")),
195
+ });
196
+
197
+ var firstResponse = await originalFetch(input, summaryInit);
198
+ if (!firstResponse || !firstResponse.ok) {
199
+ return firstResponse;
200
+ }
201
+ return originalFetch(input, linksInit);
202
+ };
203
+
204
+ globalThis.fetch = patchedFetch;
205
+ globalThis[FETCH_PATCH_KEY] = {
206
+ installed: true,
207
+ originalFetch: originalFetch,
208
+ };
209
+ }
210
+
211
+ function register(api) {
212
+ installPalzFetchPatch(api);
213
+ }
214
+
215
+ module.exports = register;
216
+ module.exports.default = register;
217
+ module.exports.id = "plugin-deliverables";
package/install.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
+ var childProcess = require("child_process");
4
5
  var fs = require("fs");
5
6
  var os = require("os");
6
7
  var path = require("path");
@@ -134,6 +135,10 @@ function samePath(a, b) {
134
135
 
135
136
  function copyRecursive(src, dst, dryRun) {
136
137
  var stat = fs.lstatSync(src);
138
+ var baseName = path.basename(src);
139
+ if (baseName.indexOf("._") === 0 || /\.tgz$/i.test(baseName)) {
140
+ return;
141
+ }
137
142
  if (stat.isSymbolicLink()) {
138
143
  var target = fs.realpathSync(src);
139
144
  copyRecursive(target, dst, dryRun);
@@ -142,7 +147,7 @@ function copyRecursive(src, dst, dryRun) {
142
147
  if (stat.isDirectory()) {
143
148
  ensureDir(dst, dryRun);
144
149
  fs.readdirSync(src).forEach(function(entry) {
145
- if (entry === "node_modules" || entry === ".git") return;
150
+ if (entry === "node_modules" || entry === ".git" || entry.indexOf("._") === 0 || /\.tgz$/i.test(entry)) return;
146
151
  copyRecursive(path.join(src, entry), path.join(dst, entry), dryRun);
147
152
  });
148
153
  return;
@@ -163,6 +168,25 @@ function replaceDirectory(src, dst, dryRun) {
163
168
  return true;
164
169
  }
165
170
 
171
+ function stagePluginRoot(pluginRoot, dryRun) {
172
+ if (dryRun) {
173
+ return {
174
+ path: pluginRoot,
175
+ cleanup: function() {}
176
+ };
177
+ }
178
+ var stageDir = fs.mkdtempSync(path.join(os.tmpdir(), "plugin-deliverables-"));
179
+ copyRecursive(pluginRoot, stageDir, false);
180
+ return {
181
+ path: stageDir,
182
+ cleanup: function() {
183
+ try {
184
+ fs.rmSync(stageDir, { recursive: true, force: true });
185
+ } catch (err) {}
186
+ }
187
+ };
188
+ }
189
+
166
190
  function normalizePluginEntryMap(entriesCfg, pluginId) {
167
191
  if (!isPlainObject(entriesCfg)) return {};
168
192
  var merged = {};
@@ -317,11 +341,25 @@ function applyPluginConfig(cfg, manifest, home) {
317
341
  if (!isPlainObject(cfg)) cfg = {};
318
342
  var pluginId = normalizeRuntimeName(manifest.name || "plugin-deliverables");
319
343
  var mcpRoot = path.join(home, "mcp-servers");
344
+ var extensionsExtraDir = path.join(home, "extensions-extra", pluginId);
320
345
 
321
346
  cfg.plugins = isPlainObject(cfg.plugins) ? cfg.plugins : {};
322
347
  cfg.plugins.allow = Array.isArray(cfg.plugins.allow) ? cfg.plugins.allow.slice() : [];
323
348
  if (cfg.plugins.allow.indexOf(pluginId) === -1) cfg.plugins.allow.push(pluginId);
324
349
  cfg.plugins.entries = isPlainObject(cfg.plugins.entries) ? cfg.plugins.entries : {};
350
+ cfg.plugins.load = isPlainObject(cfg.plugins.load) ? cfg.plugins.load : {};
351
+ var existingLoadPaths = Array.isArray(cfg.plugins.load.paths) ? cfg.plugins.load.paths.slice() : [];
352
+ var nextLoadPaths = [];
353
+ var seenLoadPaths = {};
354
+ [extensionsExtraDir].concat(existingLoadPaths).forEach(function(rawPath) {
355
+ var item = String(rawPath || "").trim();
356
+ if (!item) return;
357
+ var key = path.resolve(item);
358
+ if (seenLoadPaths[key]) return;
359
+ seenLoadPaths[key] = true;
360
+ nextLoadPaths.push(item);
361
+ });
362
+ cfg.plugins.load.paths = nextLoadPaths;
325
363
 
326
364
  var manifestPluginCfg = isPlainObject(manifest.openclaw_config) ? resolveEnvDeep(manifest.openclaw_config) : {};
327
365
  var desiredEntries = {};
@@ -392,6 +430,53 @@ function installMcpServers(pluginRoot, manifest, home, dryRun) {
392
430
  return copied;
393
431
  }
394
432
 
433
+ function restartMcpServers(manifest, home, dryRun) {
434
+ var targets = [];
435
+ if (!isPlainObject(manifest.mcp_servers)) return targets;
436
+
437
+ Object.keys(manifest.mcp_servers).forEach(function(serverName) {
438
+ var server = manifest.mcp_servers[serverName];
439
+ if (!isPlainObject(server) || typeof server.script !== "string" || !server.script.trim()) return;
440
+ targets.push(path.join(home, "mcp-servers", path.basename(server.script)));
441
+ });
442
+
443
+ if (dryRun || targets.length === 0) return [];
444
+
445
+ var raw = "";
446
+ try {
447
+ raw = childProcess.execFileSync("ps", ["-eo", "pid=,args="], {
448
+ encoding: "utf8",
449
+ stdio: ["ignore", "pipe", "ignore"]
450
+ });
451
+ } catch (err) {
452
+ return [];
453
+ }
454
+
455
+ var restarted = [];
456
+ raw.split(/\r?\n/).forEach(function(line) {
457
+ var match = line.match(/^\s*(\d+)\s+(.*)$/);
458
+ if (!match) return;
459
+ var pid = parseInt(match[1], 10);
460
+ var args = match[2] || "";
461
+ if (!pid || pid === process.pid) return;
462
+ var matchedTarget = "";
463
+ targets.some(function(target) {
464
+ if (args.indexOf(target) >= 0) {
465
+ matchedTarget = target;
466
+ return true;
467
+ }
468
+ return false;
469
+ });
470
+ if (!matchedTarget) return;
471
+ try {
472
+ process.kill(pid, "SIGTERM");
473
+ restarted.push({ pid: pid, script: matchedTarget });
474
+ } catch (err) {}
475
+ });
476
+
477
+ return restarted;
478
+ }
479
+
395
480
  function clearSessions(home, dryRun) {
396
481
  var cleared = [];
397
482
  var agentsRoot = path.join(home, "agents");
@@ -416,7 +501,10 @@ function main() {
416
501
  var home = path.resolve(args.home);
417
502
  var manifestPath = path.join(pluginRoot, "openclaw-plugin.json");
418
503
  if (!fs.existsSync(manifestPath)) {
419
- throw new Error("missing openclaw-plugin.json at " + manifestPath);
504
+ manifestPath = path.join(pluginRoot, "openclaw.plugin.json");
505
+ }
506
+ if (!fs.existsSync(manifestPath)) {
507
+ throw new Error("missing plugin manifest at " + path.join(pluginRoot, "openclaw-plugin.json"));
420
508
  }
421
509
  var manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
422
510
  var pluginName = normalizeRuntimeName(manifest.name || "plugin-deliverables");
@@ -426,32 +514,40 @@ function main() {
426
514
  var agentIDs = collectAgentIDs(home, currentConfig);
427
515
  var extensionsDir = path.join(home, "extensions", pluginName);
428
516
  var extensionsExtraDir = path.join(home, "extensions-extra", pluginName);
517
+ var staged = stagePluginRoot(pluginRoot, args.dryRun);
429
518
 
430
- ensureDir(home, args.dryRun);
431
- replaceDirectory(pluginRoot, extensionsDir, args.dryRun);
432
- replaceDirectory(pluginRoot, extensionsExtraDir, args.dryRun);
433
- var mcpFiles = installMcpServers(pluginRoot, manifest, home, args.dryRun);
434
- var skillFiles = installSkills(pluginRoot, manifest, home, workspaceRoots, agentIDs, args.dryRun);
435
- var agentFiles = injectRules(pluginRoot, manifest, workspaceRoots, args.dryRun);
436
- var nextConfig = applyPluginConfig(currentConfig, manifest, home);
437
- writeJSON(openclawPath, nextConfig, args.dryRun);
438
- var clearedSessions = args.clearSessions ? clearSessions(home, args.dryRun) : [];
439
-
440
- var summary = {
441
- plugin: pluginName,
442
- version: manifest.version || "",
443
- openclaw_home: home,
444
- dry_run: args.dryRun,
445
- copied_extensions: [extensionsDir, extensionsExtraDir],
446
- mcp_servers: mcpFiles,
447
- skill_files: skillFiles.length,
448
- agents_files: agentFiles,
449
- cleared_sessions: clearedSessions.length,
450
- openclaw_json: openclawPath
451
- };
452
- console.log(JSON.stringify(summary, null, 2));
453
- if (!args.dryRun) {
454
- console.log("Install complete. Wait about 15s for OpenClaw to reload plugins.allow, then send a new message to verify.");
519
+ try {
520
+ ensureDir(home, args.dryRun);
521
+ replaceDirectory(staged.path, extensionsDir, args.dryRun);
522
+ replaceDirectory(staged.path, extensionsExtraDir, args.dryRun);
523
+ var mcpFiles = installMcpServers(staged.path, manifest, home, args.dryRun);
524
+ var restartedMcpServers = restartMcpServers(manifest, home, args.dryRun);
525
+ var skillFiles = installSkills(staged.path, manifest, home, workspaceRoots, agentIDs, args.dryRun);
526
+ var agentFiles = injectRules(staged.path, manifest, workspaceRoots, args.dryRun);
527
+ var nextConfig = applyPluginConfig(currentConfig, manifest, home);
528
+ writeJSON(openclawPath, nextConfig, args.dryRun);
529
+ var clearedSessions = args.clearSessions ? clearSessions(home, args.dryRun) : [];
530
+
531
+ var summary = {
532
+ plugin: pluginName,
533
+ version: manifest.version || "",
534
+ openclaw_home: home,
535
+ dry_run: args.dryRun,
536
+ copied_extensions: [extensionsDir, extensionsExtraDir],
537
+ mcp_servers: mcpFiles,
538
+ restarted_mcp_servers: restartedMcpServers,
539
+ skill_files: skillFiles.length,
540
+ agents_files: agentFiles,
541
+ cleared_sessions: clearedSessions.length,
542
+ openclaw_json: openclawPath,
543
+ load_paths: nextConfig.plugins && nextConfig.plugins.load ? nextConfig.plugins.load.paths : []
544
+ };
545
+ console.log(JSON.stringify(summary, null, 2));
546
+ if (!args.dryRun) {
547
+ console.log("Install complete. Wait about 15s for OpenClaw to reload plugins.allow/plugins.load.paths, then send a new message to verify.");
548
+ }
549
+ } finally {
550
+ staged.cleanup();
455
551
  }
456
552
  }
457
553
 
@@ -14,6 +14,7 @@
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
20
  var RAW_GATEWAY_PUBLIC = (process.env.CLAW_GATEWAY_PUBLIC_URL || GATEWAY_URL).replace(/\/$/, "");
@@ -27,7 +28,7 @@ var TOOL_DEFS = [
27
28
  {
28
29
  name: "upload_deliverable",
29
30
  description: [
30
- "将 AI 生成的内容(文章、游戏、图片等)上传为交付物,返回可分享的下载/预览链接。",
31
+ "将 AI 生成的内容(文章、游戏、图片等)上传为交付物,返回一个可直接分享的访问链接。",
31
32
  "单文件交付物:提供 content_text 或 content_base64。",
32
33
  "多文件交付物(网页游戏/静态站点等):必须优先提供 files 列表,每项包含 name(相对路径)和 content_text 或 content_base64,不要先打 zip。",
33
34
  "静态多文件预览建议在根目录提供 index.html;需要单独启动端口/后端服务的项目不属于交付物预览范围,应走部署流程。",
@@ -55,7 +56,7 @@ var TOOL_DEFS = [
55
56
  },
56
57
  file_name: {
57
58
  type: "string",
58
- description: "用户可见的文件名,例如 adventure-game 或 report.md。多文件交付时这里应是目录名/项目名,不要写成 .zip,除非用户明确要求压缩包。"
59
+ description: "用户可见的文件名,必须尽量保留原始扩展名,例如 刘德华介绍.md、report.html。多文件交付时这里应是目录名/项目名,不要写成 .zip,除非用户明确要求压缩包。"
59
60
  },
60
61
  content_text: {
61
62
  type: "string",
@@ -195,6 +196,75 @@ function sanitizeFileName(v) {
195
196
  return name;
196
197
  }
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
+
198
268
  function extractUserIDFromResource(resourceID) {
199
269
  var trimmed = String(resourceID || "").trim();
200
270
  var prefix = "user_";
@@ -340,26 +410,18 @@ function formatGatewayError(statusCode, message, traceID) {
340
410
  return msg;
341
411
  }
342
412
 
343
- function buildReplyMarkdown(opts) {
413
+ function resolvePrimaryURL(opts) {
344
414
  var previewURL = opts.previewURL || "";
345
415
  var downloadURL = opts.downloadURL || "";
346
- var deliverableType = opts.type || "";
347
- var isDirectory = !!opts.isDirectory;
348
- var lines = [];
349
- if (previewURL) {
350
- lines.push("预览链接:[点击预览](" + previewURL + ")");
351
- }
352
- if (downloadURL) {
353
- if (isDirectory || deliverableType === "game") {
354
- lines.push("文件列表:[查看目录](" + downloadURL + ")");
355
- } else {
356
- lines.push("下载链接:[点击下载](" + downloadURL + ")");
357
- }
358
- }
359
- if (lines.length === 0 && !previewURL && !downloadURL) {
416
+ return previewURL || downloadURL || "";
417
+ }
418
+
419
+ function buildReplyMarkdown(opts) {
420
+ var primaryURL = resolvePrimaryURL(opts);
421
+ if (!primaryURL) {
360
422
  return "交付物已上传成功。";
361
423
  }
362
- return lines.join("\n");
424
+ return primaryURL;
363
425
  }
364
426
 
365
427
  function resolveReplyURLs(data, args, body) {
@@ -376,12 +438,13 @@ function resolveReplyURLs(data, args, body) {
376
438
  // ─── Tool implementations ─────────────────────────────────────────────────────
377
439
 
378
440
  function uploadDeliverable(args) {
441
+ var finalFileName = normalizeDeliverableFileName(args.file_name, args.type, args.content_text, args.files);
379
442
  var body = {
380
443
  resourceId: args.resource_id,
381
444
  groupId: args.group_id,
382
445
  userId: args.user_id,
383
446
  type: args.type,
384
- fileName: args.file_name,
447
+ fileName: finalFileName,
385
448
  release: "" // overwritten below after release name derivation
386
449
  };
387
450
 
@@ -426,12 +489,21 @@ function uploadDeliverable(args) {
426
489
  type: args.type,
427
490
  isDirectory: isDirectory
428
491
  });
492
+ var primaryURL = resolvePrimaryURL({
493
+ previewURL: previewURL,
494
+ downloadURL: downloadURL,
495
+ type: args.type,
496
+ isDirectory: isDirectory
497
+ });
429
498
  return {
430
499
  uuid: d.uuid,
500
+ file_name: finalFileName,
431
501
  backend: d.backend || "",
432
502
  download_url: downloadURL,
433
503
  preview_url: previewURL,
504
+ primary_url: primaryURL,
434
505
  expire_at: d.expireAt,
506
+ is_directory: isDirectory,
435
507
  reply_markdown: replyMarkdown,
436
508
  trace_id: resp.traceID || "",
437
509
  message: replyMarkdown
@@ -461,7 +533,7 @@ function handleMessage(msg) {
461
533
  send({ jsonrpc: "2.0", id: id, result: {
462
534
  protocolVersion: "2024-11-05",
463
535
  capabilities: { tools: {} },
464
- serverInfo: { name: "deliverables", version: "1.0.9" }
536
+ serverInfo: { name: "deliverables", version: "1.0.14" }
465
537
  }});
466
538
  return Promise.resolve();
467
539
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.0.9",
3
+ "version": "1.0.14",
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": {
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "plugin-deliverables",
3
+ "name": "Deliverables Plugin",
4
+ "version": "1.0.14",
5
+ "description": "Split deliverable replies into summary and link messages for Palz outbound sends.",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {}
10
+ }
11
+ }
package/package.json CHANGED
@@ -1,16 +1,24 @@
1
1
  {
2
2
  "name": "@dai_ming/plugin-deliverables",
3
- "version": "1.0.9",
4
- "description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
3
+ "version": "1.0.14",
4
+ "description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return a shareable access link",
5
5
  "keywords": [
6
6
  "openclaw",
7
7
  "plugin",
8
8
  "deliverables",
9
9
  "mcp"
10
10
  ],
11
+ "main": "index.js",
12
+ "openclaw": {
13
+ "extensions": [
14
+ "./index.js"
15
+ ]
16
+ },
11
17
  "license": "MIT",
12
18
  "files": [
19
+ "index.js",
13
20
  "install.js",
21
+ "openclaw.plugin.json",
14
22
  "openclaw-plugin.json",
15
23
  "mcp-servers/",
16
24
  "skills/",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: deliverables
3
- description: 上传AI生成的文件到交付物系统,返回可分享的预览链接
3
+ description: 上传AI生成的文件到交付物系统,返回一个可直接分享的访问链接
4
4
  ---
5
5
 
6
6
  # 文件交付规则(强制)
@@ -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` | 有意义的文件名,含扩展名 | `report-2026.html` |
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`。
@@ -65,11 +67,11 @@ files: [
65
67
  - 这段介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
66
68
  - 对于常见文章、介绍、报告类交付物,链接前的介绍通常应达到约 80-200 个中文字符,明显长于一句话确认。
67
69
  - bullet 必须写具体内容,例如“基本信息、代表作品、奖项与影响力”,不要只写“内容完整、结构清晰”这类空话。
68
- - 如果工具结果里有 `reply_markdown`,先输出 `### 访问链接`,再把它原样放在后面,不要改写其中的链接。
69
-
70
- 否则只发给用户:
71
- - 预览链接(`preview_url`,必须用 Markdown 链接格式)
72
- - 下载链接(`download_url`,必须用 Markdown 链接格式)
73
- - 多文件/游戏目录场景下,第二行也可以是 `文件列表:[查看目录](...)`,不要改成裸 URL 或 zip 描述。
74
-
75
- 不要在消息里输出文件的完整内容、工作区保存路径或裸链接,也不要只输出“一句简介 + 两个链接”这种过短格式。
70
+ - 如果工具结果里有 `reply_markdown`,先输出 `### 访问链接`,再把它原样放在后面,不要改写其中的 URL。
71
+ - 否则只输出一个最终链接:
72
+ - 优先 `primary_url`
73
+ - 没有就退回 `preview_url`
74
+ - 再没有才用 `download_url`
75
+ - 不要同时输出“预览链接 + 下载链接”两条。
76
+
77
+ 不要在消息里输出文件的完整内容、工作区保存路径,也不要只输出“一句简介 + 两个链接”这种过短格式。