@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 +15 -9
- package/agents-rules/deliverables.md +26 -44
- package/index.js +217 -0
- package/install.js +123 -27
- package/mcp-servers/deliverables.js +17 -18
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +11 -0
- package/package.json +10 -2
- package/skills/deliverables/SKILL.md +12 -18
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` |
|
|
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.
|
|
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.
|
|
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.
|
|
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 --
|
|
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.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
-
|
|
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
|
|
47
|
-
- Outputting
|
|
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,
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
86
|
-
13. If tool result contains `reply_markdown`,
|
|
87
|
-
14. Otherwise
|
|
88
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
413
|
+
function resolvePrimaryURL(opts) {
|
|
414
414
|
var previewURL = opts.previewURL || "";
|
|
415
415
|
var downloadURL = opts.downloadURL || "";
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
|
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.
|
|
536
|
+
serverInfo: { name: "deliverables", version: "1.0.14" }
|
|
538
537
|
}});
|
|
539
538
|
return Promise.resolve();
|
|
540
539
|
|
package/openclaw-plugin.json
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable
|
|
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
|
-
-
|
|
63
|
-
|
|
64
|
-
- 第二条:正常最终回复里只放链接,不加额外介绍
|
|
65
|
-
- 第一条内容介绍建议结构是:
|
|
62
|
+
- 最终消息必须先给出一段更完整的 Markdown 介绍,不要只写一句短确认。
|
|
63
|
+
- 建议结构是:
|
|
66
64
|
- 一行简短标题或总结
|
|
67
65
|
- `### 内容概览`
|
|
68
66
|
- 3-6 个 bullet,概括这份产物实际包含的章节、重点、亮点或功能
|
|
69
67
|
- 这段介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
|
|
70
|
-
-
|
|
68
|
+
- 对于常见文章、介绍、报告类交付物,链接前的介绍通常应达到约 80-200 个中文字符,明显长于一句话确认。
|
|
71
69
|
- bullet 必须写具体内容,例如“基本信息、代表作品、奖项与影响力”,不要只写“内容完整、结构清晰”这类空话。
|
|
72
|
-
-
|
|
73
|
-
-
|
|
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
|
+
不要在消息里输出文件的完整内容、工作区保存路径,也不要只输出“一句简介 + 两个链接”这种过短格式。
|