@dai_ming/plugin-deliverables 1.0.10 → 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
@@ -4,20 +4,19 @@ OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则
4
4
 
5
5
  当前版本增强点:
6
6
 
7
- - 上传成功后优先走“两条回复”体验:
8
- - 第一条是内容摘要
9
- - 第二条是纯 Markdown 链接
7
+ - 上传成功后返回标准 Markdown 链接块;同时通过运行时补丁把 palz-connector 中的“简介 + 链接”交付物回复拆成两条独立消息
10
8
  - 单文件交付物会尽量保留原始文件后缀;如果模型漏掉后缀,插件会按内容类型自动补齐(如 `.md` / `.html`)
11
9
 
12
10
  ## 包内文件
13
11
 
14
12
  | 文件 | 作用 |
15
13
  |------|------|
16
- | `install.js` | 安装器:负责复制插件、注册 MCP、安装 skill、注入 AGENTS 规则、补齐 `plugins.allow`、清 session |
14
+ | `install.js` | 安装器:负责复制插件、注册 MCP、安装 skill、注入 AGENTS 规则、补齐 `plugins.allow`、清 session,并刷新旧的 deliverables MCP 进程 |
17
15
  | `mcp-servers/deliverables.js` | MCP Server,暴露 `upload_deliverable` |
18
16
  | `skills/deliverables/SKILL.md` | 约束模型优先走交付物上传,并统一写到 `output/` |
19
17
  | `agents-rules/deliverables.md` | 注入到 `AGENTS.md` 的强约束规则 |
20
- | `openclaw-plugin.json` | 插件清单,声明 MCP、skill、rules 和运行时配置 |
18
+ | `openclaw-plugin.json` | 兼容当前安装脚本的清单,声明 MCP、skill、rules 和运行时配置 |
19
+ | `openclaw.plugin.json` | OpenClaw 运行时插件清单,用于真正加载 `index.js` 扩展 |
21
20
 
22
21
  ## 推荐安装方式
23
22
 
@@ -27,7 +26,7 @@ OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则
27
26
 
28
27
  ```yaml
29
28
  installPlugins:
30
- - "@dai_ming/plugin-deliverables@1.0.10"
29
+ - "@dai_ming/plugin-deliverables@1.0.14"
31
30
  ```
32
31
 
33
32
  现有 chart 会在 init 阶段完成安装。
@@ -38,7 +37,7 @@ installPlugins:
38
37
 
39
38
  ```bash
40
39
  npm config set registry https://registry.npmmirror.com
41
- npm install @dai_ming/plugin-deliverables@1.0.10
40
+ npm install @dai_ming/plugin-deliverables@1.0.14
42
41
  node node_modules/@dai_ming/plugin-deliverables/install.js
43
42
  ```
44
43
 
@@ -64,9 +63,16 @@ node node_modules/@dai_ming/plugin-deliverables/install.js \
64
63
  - 注册 `mcp.servers.deliverables`
65
64
  - 启用 `skills.entries.deliverables`
66
65
  - 启用 `plugins.entries.plugin-deliverables`
66
+ - 把 `~/.openclaw/extensions-extra/plugin-deliverables` 写入 `plugins.load.paths`
67
67
  - 把 `plugin-deliverables` 加入 `plugins.allow`
68
68
  7. 清理 session,让下一条消息重新读取 prompt / skill / tool 配置
69
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` 原地执行安装时把源目录自己删掉
70
76
 
71
77
  ## 是否需要重启 Pod
72
78
 
@@ -74,7 +80,7 @@ node node_modules/@dai_ming/plugin-deliverables/install.js \
74
80
 
75
81
  原因:
76
82
  - 安装脚本会直接改 `~/.openclaw/openclaw.json`
77
- - `plugins.allow` 变化会触发 OpenClaw 的 watcher,进程级重载大约 15 秒
83
+ - `plugins.allow` / `plugins.load.paths` 变化会触发 OpenClaw 的 watcher,进程级重载大约 15 秒
78
84
  - session 也会被清掉,所以下一条消息会重新读取最新的 `AGENTS.md` / skill
79
85
 
80
86
  建议做法:
@@ -143,4 +149,4 @@ curl -H "X-API-Key: $CLAW_GATEWAY_API_KEY" "$CLAW_GATEWAY_URL/healthz"
143
149
 
144
150
  ### 返回的是 `/preview/:uuid` 或内部地址
145
151
 
146
- `1.0.10` 起,插件会在 `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,5 +1,5 @@
1
1
  <!-- DELIVERABLE_LINK_RULES_START -->
2
- ## Deliverables -- Two Reply Link Rule (HARD CONSTRAINT)
2
+ ## Deliverables -- URL Echo Rule (HARD CONSTRAINT)
3
3
 
4
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
 
@@ -12,40 +12,28 @@ Tool name note:
12
12
 
13
13
  ### Required output behavior
14
14
 
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:
29
- `预览链接:[点击预览](<full_url>)`
30
- 5. If tool result has `download_url`, include one line:
31
- `下载链接:[点击下载](<full_url>)`
32
- 6. For multi-file/game deliverables, if tool result already formats the second line as
33
- `文件列表:[查看目录](<full_url>)`
34
- then keep that exact label and URL. Do not rewrite it back to a raw long link.
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.
15
+ 0. Your final assistant message MUST start with a richer Markdown intro section, not a single short confirmation sentence.
16
+ 1. The intro must be based on the actual content/request, not a fixed boilerplate like `交付物已上传成功,可直接在线预览或下载。`
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 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.
39
29
 
40
30
  ### Forbidden output behavior
41
31
 
42
32
  - "可点击预览链接查看" but no actual URL
43
- - Putting links into the first summary reply
44
- - Putting extra narrative text into the second pure-links reply
33
+ - A single short sentence plus links when the deliverable is an article/report/introduction and concrete highlights are available
45
34
  - Replacing URL target with a non-original URL
46
- - Omitting both links when tool already returned links
47
- - Outputting only naked long URL without Markdown link label
48
- - 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
49
37
  - Using a generic boilerplate intro that does not mention the actual deliverable content
50
38
  <!-- DELIVERABLE_LINK_RULES_END -->
51
39
 
@@ -74,24 +62,18 @@ Tool name note:
74
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.
75
63
  7. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
76
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.
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:
65
+ 9. After successful upload, the final assistant message MUST start with a richer Markdown intro section, not just one short sentence.
66
+ 10. The intro must be based on the actual deliverable content/request, not a fixed sentence like `交付物已上传成功,可直接在线预览或下载。`
67
+ 11. Prefer this Markdown structure:
82
68
  - a short title or summary line
83
69
  - `### 内容概览`
84
70
  - 3-6 bullet points summarizing the actual content sections, highlights, or key features
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):
88
- `预览链接:[点击预览](<full_url>)`
89
- `下载链接:[点击下载](<full_url>)`
90
- 15. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
91
- `文件列表:[查看目录](<full_url>)`
92
- Keep that format instead of forcing a zip link.
71
+ 12. For ordinary articles/reports/introductions, the intro before the links should normally reach roughly 80-200 Chinese characters total.
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 说明。
93
75
  16. Do NOT only say "已保存到工作空间".
94
- 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.
95
77
 
96
78
  ### Exception
97
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
 
@@ -28,7 +28,7 @@ var TOOL_DEFS = [
28
28
  {
29
29
  name: "upload_deliverable",
30
30
  description: [
31
- "将 AI 生成的内容(文章、游戏、图片等)上传为交付物,返回可分享的下载/预览链接。",
31
+ "将 AI 生成的内容(文章、游戏、图片等)上传为交付物,返回一个可直接分享的访问链接。",
32
32
  "单文件交付物:提供 content_text 或 content_base64。",
33
33
  "多文件交付物(网页游戏/静态站点等):必须优先提供 files 列表,每项包含 name(相对路径)和 content_text 或 content_base64,不要先打 zip。",
34
34
  "静态多文件预览建议在根目录提供 index.html;需要单独启动端口/后端服务的项目不属于交付物预览范围,应走部署流程。",
@@ -410,26 +410,18 @@ function formatGatewayError(statusCode, message, traceID) {
410
410
  return msg;
411
411
  }
412
412
 
413
- function buildReplyMarkdown(opts) {
413
+ function resolvePrimaryURL(opts) {
414
414
  var previewURL = opts.previewURL || "";
415
415
  var downloadURL = opts.downloadURL || "";
416
- var deliverableType = opts.type || "";
417
- var isDirectory = !!opts.isDirectory;
418
- var lines = [];
419
- if (previewURL) {
420
- lines.push("预览链接:[点击预览](" + previewURL + ")");
421
- }
422
- if (downloadURL) {
423
- if (isDirectory || deliverableType === "game") {
424
- lines.push("文件列表:[查看目录](" + downloadURL + ")");
425
- } else {
426
- lines.push("下载链接:[点击下载](" + downloadURL + ")");
427
- }
428
- }
429
- if (lines.length === 0 && !previewURL && !downloadURL) {
416
+ return previewURL || downloadURL || "";
417
+ }
418
+
419
+ function buildReplyMarkdown(opts) {
420
+ var primaryURL = resolvePrimaryURL(opts);
421
+ if (!primaryURL) {
430
422
  return "交付物已上传成功。";
431
423
  }
432
- return lines.join("\n");
424
+ return primaryURL;
433
425
  }
434
426
 
435
427
  function resolveReplyURLs(data, args, body) {
@@ -497,12 +489,19 @@ function uploadDeliverable(args) {
497
489
  type: args.type,
498
490
  isDirectory: isDirectory
499
491
  });
492
+ var primaryURL = resolvePrimaryURL({
493
+ previewURL: previewURL,
494
+ downloadURL: downloadURL,
495
+ type: args.type,
496
+ isDirectory: isDirectory
497
+ });
500
498
  return {
501
499
  uuid: d.uuid,
502
500
  file_name: finalFileName,
503
501
  backend: d.backend || "",
504
502
  download_url: downloadURL,
505
503
  preview_url: previewURL,
504
+ primary_url: primaryURL,
506
505
  expire_at: d.expireAt,
507
506
  is_directory: isDirectory,
508
507
  reply_markdown: replyMarkdown,
@@ -534,7 +533,7 @@ function handleMessage(msg) {
534
533
  send({ jsonrpc: "2.0", id: id, result: {
535
534
  protocolVersion: "2024-11-05",
536
535
  capabilities: { tools: {} },
537
- serverInfo: { name: "deliverables", version: "1.0.10" }
536
+ serverInfo: { name: "deliverables", version: "1.0.14" }
538
537
  }});
539
538
  return Promise.resolve();
540
539
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.0.10",
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.10",
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
  # 文件交付规则(强制)
@@ -59,25 +59,19 @@ files: [
59
59
  ## 上传成功后
60
60
 
61
61
  优先规则:
62
- - 如果当前会话里存在一个名字就叫 `message` 的工具,上传成功后必须发两条可见回复:
63
- - 第一条:通过 `message` 工具发送,只写内容介绍,不放链接
64
- - 第二条:正常最终回复里只放链接,不加额外介绍
65
- - 第一条内容介绍建议结构是:
62
+ - 最终消息必须先给出一段更完整的 Markdown 介绍,不要只写一句短确认。
63
+ - 建议结构是:
66
64
  - 一行简短标题或总结
67
65
  - `### 内容概览`
68
66
  - 3-6 个 bullet,概括这份产物实际包含的章节、重点、亮点或功能
69
67
  - 这段介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
70
- - 对于常见文章、介绍、报告类交付物,第一条介绍通常应达到约 80-220 个中文字符,明显长于一句话确认。
68
+ - 对于常见文章、介绍、报告类交付物,链接前的介绍通常应达到约 80-200 个中文字符,明显长于一句话确认。
71
69
  - bullet 必须写具体内容,例如“基本信息、代表作品、奖项与影响力”,不要只写“内容完整、结构清晰”这类空话。
72
- - 第一条介绍里不要放工作区路径、裸链接、Markdown 链接或下载提示。
73
- - 第二条链接回复里,如果工具结果有 `reply_markdown`,直接原样输出它,不要改写,不要再加标题或说明。
74
- - 第二条链接回复只允许包含:
75
- - `预览链接:[点击预览](...)`
76
- - `下载链接:[点击下载](...)`
77
- - 或目录场景下的 `文件列表:[查看目录](...)`
78
- - 不要把第一条和第二条混在一起,也不要在第二条链接消息里再补“已完成”“请查看”等多余文字。
79
-
80
- 回退规则:
81
- - 如果当前会话没有 `message` 工具,再退回成一条最终 Markdown 回复:先介绍,再附链接。
82
-
83
- 不要在消息里输出文件的完整内容、工作区保存路径或裸链接,也不要只输出“一句简介 + 两个链接”这种过短格式。
70
+ - 如果工具结果里有 `reply_markdown`,先输出 `### 访问链接`,再把它原样放在后面,不要改写其中的 URL。
71
+ - 否则只输出一个最终链接:
72
+ - 优先 `primary_url`
73
+ - 没有就退回 `preview_url`
74
+ - 再没有才用 `download_url`
75
+ - 不要同时输出“预览链接 + 下载链接”两条。
76
+
77
+ 不要在消息里输出文件的完整内容、工作区保存路径,也不要只输出“一句简介 + 两个链接”这种过短格式。