@dai_ming/plugin-deliverables 1.0.0
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 +214 -0
- package/agents-rules/deliverables.md +77 -0
- package/mcp-servers/deliverables.js +295 -0
- package/openclaw-plugin.json +33 -0
- package/package.json +21 -0
- package/skills/deliverables/SKILL.md +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# @dai_ming/plugin-deliverables
|
|
2
|
+
|
|
3
|
+
OpenClaw 交付物插件 — 让 AI Agent 将生成的文件(文章、HTML 页面、多文件游戏/网站、图片等)自动上传到 OSS,并在会话消息中回显可点击的预览/下载链接。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 插件包含什么
|
|
8
|
+
|
|
9
|
+
| 文件 | 作用 |
|
|
10
|
+
|------|------|
|
|
11
|
+
| `mcp-servers/deliverables.js` | MCP Server 脚本:实现 `upload_deliverable` 工具,通过 HTTP 调用 claw-gateway API 上传文件 |
|
|
12
|
+
| `skills/deliverables/SKILL.md` | OpenClaw Skill:强制文档产出走 `upload_deliverable`,规范写入路径和参数 |
|
|
13
|
+
| `agents-rules/deliverables.md` | AGENTS.md 规则块:URL 回显规则 + 自动上传规则(硬约束,注入到每个 workspace 的 AGENTS.md) |
|
|
14
|
+
| `openclaw-plugin.json` | 插件清单:声明 MCP server、skill、AGENTS 规则、openclaw.json 配置段 |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 安装方式
|
|
19
|
+
|
|
20
|
+
### 方式一:通过 claw-gateway Helm 部署(推荐)
|
|
21
|
+
|
|
22
|
+
只需在 Helm values 里把插件加入 `installPlugins` 列表,gateway 和 Helm initContainer 会自动完成所有配置:
|
|
23
|
+
|
|
24
|
+
```yaml
|
|
25
|
+
# values.yaml(或 claw-gateway 管理界面的 Helm 参数)
|
|
26
|
+
installPlugins:
|
|
27
|
+
- "@dai_ming/plugin-deliverables@1.0.0"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
initContainer 执行顺序:
|
|
31
|
+
1. **Phase 3**:`npm pack @dai_ming/plugin-deliverables@1.0.0` 下载 tarball → 解压到 `/data/extensions-extra/plugin-deliverables/`
|
|
32
|
+
2. **Phase 3b**:在插件目录执行 `npm install --omit=dev`(本插件无运行时依赖,此步骤跳过)
|
|
33
|
+
3. **Phase 3e**:读取 `openclaw-plugin.json` 清单,自动:
|
|
34
|
+
- 复制 `mcp-servers/deliverables.js` → `/data/mcp-servers/deliverables.js`
|
|
35
|
+
- 安装 `skills/deliverables/SKILL.md` → 所有 workspace 的 `skills/deliverables/SKILL.md`
|
|
36
|
+
- 注入 `agents-rules/deliverables.md` 内容到所有 workspace 的 `AGENTS.md`(幂等,按 marker 替换)
|
|
37
|
+
|
|
38
|
+
> **env 变量**:MCP Server 需要以下变量(claw-gateway 在生成 `mcp.servers` 配置时会自动注入):
|
|
39
|
+
>
|
|
40
|
+
> | 变量 | 说明 | 默认值 |
|
|
41
|
+
> |------|------|--------|
|
|
42
|
+
> | `CLAW_GATEWAY_URL` | gateway 内部地址 | `http://claw-gateway:8080` |
|
|
43
|
+
> | `CLAW_GATEWAY_PUBLIC_URL` | gateway 公网地址(用于生成预览链接) | 同上 |
|
|
44
|
+
> | `CLAW_GATEWAY_API_KEY` | API Key | `api-key-1` |
|
|
45
|
+
|
|
46
|
+
### 方式二:手动安装(不使用 claw-gateway 自动部署)
|
|
47
|
+
|
|
48
|
+
适用于自行管理 OpenClaw 容器的场景。
|
|
49
|
+
|
|
50
|
+
**Step 1 — 下载并解压插件**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm config set registry https://registry.npmmirror.com
|
|
54
|
+
mkdir -p ~/.openclaw/extensions-extra/plugin-deliverables
|
|
55
|
+
cd /tmp && npm pack @dai_ming/plugin-deliverables
|
|
56
|
+
tar xzf openclaw-plugin-deliverables-*.tgz -C ~/.openclaw/extensions-extra/plugin-deliverables --strip-components=1
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Step 2 — 复制 MCP Server 脚本**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
mkdir -p ~/.openclaw/mcp-servers
|
|
63
|
+
cp ~/.openclaw/extensions-extra/plugin-deliverables/mcp-servers/deliverables.js \
|
|
64
|
+
~/.openclaw/mcp-servers/deliverables.js
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Step 3 — 安装 Skill**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
WORKSPACE=~/.openclaw/workspace # 替换为实际 workspace 路径
|
|
71
|
+
mkdir -p "$WORKSPACE/skills/deliverables"
|
|
72
|
+
cp ~/.openclaw/extensions-extra/plugin-deliverables/skills/deliverables/SKILL.md \
|
|
73
|
+
"$WORKSPACE/skills/deliverables/SKILL.md"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Step 4 — 注入 AGENTS.md 规则**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
AGENTS="$WORKSPACE/AGENTS.md"
|
|
80
|
+
RULES=~/.openclaw/extensions-extra/plugin-deliverables/agents-rules/deliverables.md
|
|
81
|
+
|
|
82
|
+
if grep -q "DELIVERABLE_LINK_RULES_START" "$AGENTS" 2>/dev/null; then
|
|
83
|
+
# 已有旧规则,用 Node.js 按 marker 替换
|
|
84
|
+
node -e "
|
|
85
|
+
var fs=require('fs');
|
|
86
|
+
var a=fs.readFileSync('$AGENTS','utf8');
|
|
87
|
+
var r=fs.readFileSync('$RULES','utf8').trim();
|
|
88
|
+
var re=/<!--\s*DELIVERABLE_LINK_RULES_START\s*-->[\s\S]*?<!--\s*DELIVERABLE_AUTO_UPLOAD_RULES_END\s*-->/g;
|
|
89
|
+
fs.writeFileSync('$AGENTS', re.test(a) ? a.replace(re,r) : a+'\n\n'+r+'\n');
|
|
90
|
+
"
|
|
91
|
+
else
|
|
92
|
+
# 首次注入
|
|
93
|
+
printf '\n\n' >> "$AGENTS"
|
|
94
|
+
cat "$RULES" >> "$AGENTS"
|
|
95
|
+
fi
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Step 5 — 配置 openclaw.json**
|
|
99
|
+
|
|
100
|
+
在 `~/.openclaw/openclaw.json` 的 `mcp.servers` 下添加:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcp": {
|
|
105
|
+
"servers": {
|
|
106
|
+
"deliverables": {
|
|
107
|
+
"command": "node",
|
|
108
|
+
"args": ["/home/node/.openclaw/mcp-servers/deliverables.js"],
|
|
109
|
+
"env": {
|
|
110
|
+
"CLAW_GATEWAY_URL": "http://<your-gateway-host>:8080",
|
|
111
|
+
"CLAW_GATEWAY_PUBLIC_URL": "https://<public-domain>",
|
|
112
|
+
"CLAW_GATEWAY_API_KEY": "<your-api-key>"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"plugins": {
|
|
118
|
+
"entries": {
|
|
119
|
+
"deliverables": { "enabled": true }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## AGENTS.md 规则说明
|
|
128
|
+
|
|
129
|
+
插件向每个 workspace 的 `AGENTS.md` 注入两段硬约束规则:
|
|
130
|
+
|
|
131
|
+
| 规则段 | Marker | 作用 |
|
|
132
|
+
|--------|--------|------|
|
|
133
|
+
| URL 回显规则 | `DELIVERABLE_LINK_RULES_START` … `DELIVERABLE_LINK_RULES_END` | 强制 Agent 上传后在消息中输出可点击的 Markdown 链接 |
|
|
134
|
+
| 自动上传规则 | `DELIVERABLE_AUTO_UPLOAD_RULES_START` … `DELIVERABLE_AUTO_UPLOAD_RULES_END` | 当用户要求生成文件时默认自动上传,写入 `output/` 目录 |
|
|
135
|
+
|
|
136
|
+
规则使用 HTML 注释 Marker 包裹,initContainer 和 Guardian 进程每次同步时都会幂等替换,不会产生重复块。
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Skill 说明
|
|
141
|
+
|
|
142
|
+
`skills/deliverables/SKILL.md` 是 OpenClaw Skill,被注入到 workspace 后由 Agent 在工具调用前读取。它规定:
|
|
143
|
+
|
|
144
|
+
- 始终使用会话中实际暴露的工具名(`deliverables__upload_deliverable`)
|
|
145
|
+
- 生成文件统一写入 `output/` 子目录
|
|
146
|
+
- 多文件(游戏/网站)优先用 `type=game` + `files[]`,不要提前打 zip
|
|
147
|
+
- 上传成功后回复必须附带 Markdown 格式的预览/下载链接
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## MCP Server 说明
|
|
152
|
+
|
|
153
|
+
`mcp-servers/deliverables.js` 是一个符合 [MCP 协议(2024-11-05)](https://modelcontextprotocol.io/) 的 Node.js stdio server,暴露单个工具:
|
|
154
|
+
|
|
155
|
+
### `upload_deliverable`
|
|
156
|
+
|
|
157
|
+
| 参数 | 类型 | 必填 | 说明 |
|
|
158
|
+
|------|------|------|------|
|
|
159
|
+
| `resource_id` | string | ✅ | 当前会话唯一 ID |
|
|
160
|
+
| `type` | enum | ✅ | `article` / `game` / `zip` / `image` / `video` / `ppt` / `link` |
|
|
161
|
+
| `file_name` | string | ✅ | 用户可见文件名(含扩展名)或目录名 |
|
|
162
|
+
| `group_id` | string | — | 群聊 ID |
|
|
163
|
+
| `user_id` | string | — | 用户 ID |
|
|
164
|
+
| `content_text` | string | — | 文本内容(单文件场景) |
|
|
165
|
+
| `content_base64` | string | — | Base64 编码的二进制内容(单文件场景) |
|
|
166
|
+
| `files` | array | — | 多文件列表(游戏/站点场景,优先使用) |
|
|
167
|
+
|
|
168
|
+
返回值包含 `preview_url`、`download_url`、`reply_markdown`(可直接附在回复消息中)。
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 版本与发布
|
|
173
|
+
|
|
174
|
+
### 在 claw-gateway 仓库内发布
|
|
175
|
+
|
|
176
|
+
插件源码位于 `package/` 目录,与 claw-gateway 主仓库同步维护。更新流程:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
# 1. 修改 package/package.json 中的 version
|
|
180
|
+
# 2. 同步更新 package/mcp-servers/deliverables.js(如有变更)
|
|
181
|
+
# 3. 打包并发布到 npmmirror 兼容的私有或公开 registry
|
|
182
|
+
cd package
|
|
183
|
+
npm publish --registry https://registry.npmmirror.com
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 版本对齐建议
|
|
187
|
+
|
|
188
|
+
| claw-gateway 版本 | plugin 版本 |
|
|
189
|
+
|-------------------|-------------|
|
|
190
|
+
| 当前 | 1.0.0 |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 常见问题
|
|
195
|
+
|
|
196
|
+
**Q: 插件安装后 Agent 没有 `upload_deliverable` 工具怎么办?**
|
|
197
|
+
|
|
198
|
+
检查以下几点:
|
|
199
|
+
1. `openclaw.json` 的 `mcp.servers.deliverables` 是否存在
|
|
200
|
+
2. MCP Server 是否在运行:`kubectl exec <pod> -- ls /home/node/.openclaw/mcp-servers/`
|
|
201
|
+
3. Agent 的 `tools.allow` 是否包含 `deliverables__upload_deliverable`
|
|
202
|
+
|
|
203
|
+
**Q: 上传后没有返回链接怎么办?**
|
|
204
|
+
|
|
205
|
+
确认 `CLAW_GATEWAY_URL` 和 `CLAW_GATEWAY_API_KEY` 注入正确,可在容器内测试:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
curl -H "X-API-Key: $CLAW_GATEWAY_API_KEY" $CLAW_GATEWAY_URL/healthz
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Q: claw-gateway 已经内置了 deliverables,会和插件冲突吗?**
|
|
212
|
+
|
|
213
|
+
不会冲突。claw-gateway 通过 ConfigMap 注入的 MCP 脚本、skill、AGENTS 规则与插件内容完全一致。
|
|
214
|
+
Phase 3e(插件注入)在前,ConfigMap 阶段(Phase 4/4c/4d)在后;ConfigMap 的内容会覆盖插件安装的内容,两者互为备份。
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!-- DELIVERABLE_LINK_RULES_START -->
|
|
2
|
+
## Deliverables -- URL Echo Rule (HARD CONSTRAINT)
|
|
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).
|
|
5
|
+
|
|
6
|
+
Tool name note:
|
|
7
|
+
|
|
8
|
+
- Always call the exact tool name exposed in the current session.
|
|
9
|
+
- In current runtime, the deliverables tool is usually:
|
|
10
|
+
- `deliverables__upload_deliverable`
|
|
11
|
+
- If notes mention bare names like `upload_deliverable`, do **not** guess. Use the actual namespaced tool that is exposed to you.
|
|
12
|
+
|
|
13
|
+
### Required output behavior
|
|
14
|
+
|
|
15
|
+
0. Your final assistant message MUST start with 1-2 short sentences in your own words, briefly describing what you produced for the user.
|
|
16
|
+
1. The intro must be based on the actual content/request, not a fixed boilerplate like `交付物已上传成功,可直接在线预览或下载。`
|
|
17
|
+
2. If tool result contains `reply_markdown`, append that field verbatim after your intro on the following lines. Do not rewrite the links inside it.
|
|
18
|
+
3. If tool result has `preview_url`, include one line:
|
|
19
|
+
`预览链接:[点击预览](<full_url>)`
|
|
20
|
+
4. If tool result has `download_url`, include one line:
|
|
21
|
+
`下载链接:[点击下载](<full_url>)`
|
|
22
|
+
5. For multi-file/game deliverables, if tool result already formats the second line as
|
|
23
|
+
`文件列表:[查看目录](<full_url>)`
|
|
24
|
+
then keep that exact label and URL. Do not rewrite it back to a raw long link.
|
|
25
|
+
6. Keep URL target value exactly from tool result (no shortening, no masking, no redirect rewrite).
|
|
26
|
+
7. Use short link labels (`点击预览` / `点击下载` / `查看目录`) to avoid exposing long raw URLs.
|
|
27
|
+
8. Do not say "已上传/已完成" without links.
|
|
28
|
+
9. Do not output naked long URLs outside Markdown link syntax.
|
|
29
|
+
10. Keep the intro concise; do not paste the full file content, saved-path text, or a long summary after the Markdown links.
|
|
30
|
+
|
|
31
|
+
### Forbidden output behavior
|
|
32
|
+
|
|
33
|
+
- "可点击预览链接查看" but no actual URL
|
|
34
|
+
- Replacing URL target with a non-original URL
|
|
35
|
+
- Omitting both links when tool already returned links
|
|
36
|
+
- Outputting only naked long URL without Markdown link label
|
|
37
|
+
- Replacing `文件列表:[查看目录](...)` with a naked URL or a zip description
|
|
38
|
+
- Using a generic boilerplate intro that does not mention the actual deliverable content
|
|
39
|
+
<!-- DELIVERABLE_LINK_RULES_END -->
|
|
40
|
+
|
|
41
|
+
<!-- DELIVERABLE_AUTO_UPLOAD_RULES_START -->
|
|
42
|
+
## Deliverables -- Auto Upload for Document Requests (HARD CONSTRAINT)
|
|
43
|
+
|
|
44
|
+
When user asks to write/generate/create a file, document, article, report, HTML, PPT, plan, guide, markdown, introduction, biography, strategy, summary, or similar artifact, you MUST upload it as a deliverable by default.
|
|
45
|
+
|
|
46
|
+
Tool name note:
|
|
47
|
+
|
|
48
|
+
- Always call the exact tool name exposed in the current session.
|
|
49
|
+
- In current runtime, the upload tool is `deliverables__upload_deliverable`.
|
|
50
|
+
- Do not call a guessed bare tool name if the actual tool list shows a namespaced one.
|
|
51
|
+
|
|
52
|
+
### Required behavior
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
2. If you need to use the `write` tool, the target path MUST be inside `output/`. Never write deliverable files to the workspace root.
|
|
56
|
+
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.
|
|
57
|
+
4. If format is HTML, prefer `type=article` and file name ends with `.html`.
|
|
58
|
+
5. If format is markdown/text, use `type=article` and `.md`/`.txt`.
|
|
59
|
+
6. For multi-file deliverables (game/site), you MUST prefer `type=game` with `files[]`, keep files as a folder structure, and do NOT zip before upload unless the user explicitly asks for a zip package.
|
|
60
|
+
7. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
|
|
61
|
+
8. If a generated project requires starting a separate backend service, custom port, database, or long-running process to work, do NOT pretend the deliverables preview can run it. Tell the user that deliverables preview only supports static output, and that runtime projects need deployment/ingress instead.
|
|
62
|
+
9. After successful upload, the final assistant message MUST start with 1-2 short sentences in your own words, briefly describing what you generated for the user.
|
|
63
|
+
10. The intro must be based on the actual deliverable content/request, not a fixed sentence like `交付物已上传成功,可直接在线预览或下载。`
|
|
64
|
+
11. If tool result contains `reply_markdown`, append that field verbatim after your intro on the following lines.
|
|
65
|
+
12. Otherwise final reply MUST include Markdown links (short label + full URL target):
|
|
66
|
+
`预览链接:[点击预览](<full_url>)`
|
|
67
|
+
`下载链接:[点击下载](<full_url>)`
|
|
68
|
+
13. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
|
|
69
|
+
`文件列表:[查看目录](<full_url>)`
|
|
70
|
+
Keep that format instead of forcing a zip link.
|
|
71
|
+
14. Do NOT only say "已保存到工作空间".
|
|
72
|
+
15. Do NOT append workspace path or a raw URL block after the Markdown links.
|
|
73
|
+
|
|
74
|
+
### Exception
|
|
75
|
+
|
|
76
|
+
- Only skip upload when user explicitly says: "不要上传交付物" / "只保存在工作区".
|
|
77
|
+
<!-- DELIVERABLE_AUTO_UPLOAD_RULES_END -->
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* deliverables MCP Server
|
|
6
|
+
*
|
|
7
|
+
* Provides tools for uploading AI-generated content (articles, games, images, etc.)
|
|
8
|
+
* to OSS via the claw-gateway API and returning shareable URLs.
|
|
9
|
+
*
|
|
10
|
+
* Env vars (injected by helm_deployer.go):
|
|
11
|
+
* CLAW_GATEWAY_URL — base URL of claw-gateway, e.g. http://claw-gateway:8080
|
|
12
|
+
* CLAW_GATEWAY_API_KEY / OPENCLAW_GATEWAY_API_KEY — API key with access to /openclaw-gateway/be/*
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
var http = require("http");
|
|
16
|
+
var https = require("https");
|
|
17
|
+
|
|
18
|
+
var GATEWAY_URL = (process.env.CLAW_GATEWAY_URL || "http://claw-gateway:8080").replace(/\/$/, "");
|
|
19
|
+
var GATEWAY_PUBLIC = (process.env.CLAW_GATEWAY_PUBLIC_URL || GATEWAY_URL).replace(/\/$/, "");
|
|
20
|
+
// Some existing pods were created without CLAW_GATEWAY_API_KEY injected.
|
|
21
|
+
// Keep env-driven behavior first, but provide a dev-compatible fallback to avoid
|
|
22
|
+
// breaking deliverable uploads during rolling migration.
|
|
23
|
+
var API_KEY = process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY || "api-key-1";
|
|
24
|
+
|
|
25
|
+
var TOOL_DEFS = [
|
|
26
|
+
{
|
|
27
|
+
name: "upload_deliverable",
|
|
28
|
+
description: [
|
|
29
|
+
"将 AI 生成的内容(文章、游戏、图片等)上传为交付物,返回可分享的下载/预览链接。",
|
|
30
|
+
"单文件交付物:提供 content_text 或 content_base64。",
|
|
31
|
+
"多文件交付物(网页游戏/静态站点等):必须优先提供 files 列表,每项包含 name(相对路径)和 content_text 或 content_base64,不要先打 zip。",
|
|
32
|
+
"静态多文件预览建议在根目录提供 index.html;需要单独启动端口/后端服务的项目不属于交付物预览范围,应走部署流程。",
|
|
33
|
+
"返回的 download_url / preview_url 为长期可用链接。"
|
|
34
|
+
].join(" "),
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
resource_id: {
|
|
39
|
+
type: "string",
|
|
40
|
+
description: "当前聊天框/会话的唯一 ID(必填)。对应消息上下文中的 resource_id 字段,直接使用该值即可。"
|
|
41
|
+
},
|
|
42
|
+
group_id: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "当前会话所属的群聊 ID(可选)"
|
|
45
|
+
},
|
|
46
|
+
user_id: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "请求交付物的用户 ID(可选)"
|
|
49
|
+
},
|
|
50
|
+
type: {
|
|
51
|
+
type: "string",
|
|
52
|
+
enum: ["article", "game", "zip", "image", "video", "ppt", "link"],
|
|
53
|
+
description: "交付物类型:article=文章/文档,game=静态多文件网页/游戏(优先使用该类型并传 files,除非用户明确要 zip),zip=通用压缩包,image=图片,video=视频,ppt=演示文稿,link=外部链接(无需上传内容,fileName 填 URL)"
|
|
54
|
+
},
|
|
55
|
+
file_name: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "用户可见的文件名,例如 adventure-game 或 report.md。多文件交付时这里应是目录名/项目名,不要写成 .zip,除非用户明确要求压缩包。"
|
|
58
|
+
},
|
|
59
|
+
content_text: {
|
|
60
|
+
type: "string",
|
|
61
|
+
description: "文本内容(文章、HTML、Markdown 等),单文件时使用"
|
|
62
|
+
},
|
|
63
|
+
content_base64: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "Base64 编码的二进制内容(图片、zip 等),单文件时使用"
|
|
66
|
+
},
|
|
67
|
+
files: {
|
|
68
|
+
type: "array",
|
|
69
|
+
description: "多文件列表(游戏/静态站点场景)必须优先使用,服务端会保留目录结构,不再打 zip。静态预览场景建议包含根目录 index.html。",
|
|
70
|
+
items: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: {
|
|
73
|
+
name: { type: "string", description: "文件在交付物目录内的相对路径,例如 index.html 或 assets/main.js" },
|
|
74
|
+
content_text: { type: "string", description: "文本内容" },
|
|
75
|
+
content_base64: { type: "string", description: "Base64 二进制内容" }
|
|
76
|
+
},
|
|
77
|
+
required: ["name"]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
required: ["resource_id", "type", "file_name"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// ─── HTTP helpers ─────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function parseURL(rawURL) {
|
|
89
|
+
try {
|
|
90
|
+
return new URL(rawURL);
|
|
91
|
+
} catch(e) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function httpRequest(method, path, body) {
|
|
97
|
+
return new Promise(function(resolve, reject) {
|
|
98
|
+
var parsed = parseURL(GATEWAY_URL + path);
|
|
99
|
+
if (!parsed) {
|
|
100
|
+
return reject(new Error("invalid gateway URL: " + GATEWAY_URL + path));
|
|
101
|
+
}
|
|
102
|
+
var isHTTPS = parsed.protocol === "https:";
|
|
103
|
+
var transport = isHTTPS ? https : http;
|
|
104
|
+
var bodyStr = body ? JSON.stringify(body) : "";
|
|
105
|
+
var options = {
|
|
106
|
+
hostname: parsed.hostname,
|
|
107
|
+
port: parsed.port || (isHTTPS ? 443 : 80),
|
|
108
|
+
path: parsed.pathname + (parsed.search || ""),
|
|
109
|
+
method: method,
|
|
110
|
+
headers: {
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
"X-API-Key": API_KEY,
|
|
113
|
+
"Content-Length": Buffer.byteLength(bodyStr)
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
var req = transport.request(options, function(res) {
|
|
117
|
+
var chunks = [];
|
|
118
|
+
var traceID = res.headers["x-trace-id"] || "";
|
|
119
|
+
res.on("data", function(c) { chunks.push(c); });
|
|
120
|
+
res.on("end", function() {
|
|
121
|
+
var text = Buffer.concat(chunks).toString();
|
|
122
|
+
try {
|
|
123
|
+
var obj = JSON.parse(text);
|
|
124
|
+
if (res.statusCode >= 400) {
|
|
125
|
+
reject(new Error(formatGatewayError(res.statusCode, obj.message || text, traceID)));
|
|
126
|
+
} else {
|
|
127
|
+
resolve({ body: obj, traceID: traceID });
|
|
128
|
+
}
|
|
129
|
+
} catch(e) {
|
|
130
|
+
reject(new Error(formatGatewayError(res.statusCode, "non-JSON response: " + text.slice(0, 200), traceID)));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
req.on("error", reject);
|
|
135
|
+
if (bodyStr) req.write(bodyStr);
|
|
136
|
+
req.end();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatGatewayError(statusCode, message, traceID) {
|
|
141
|
+
var msg = "gateway " + statusCode + ": " + message;
|
|
142
|
+
if (traceID) {
|
|
143
|
+
msg += " (trace_id=" + traceID + ")";
|
|
144
|
+
}
|
|
145
|
+
return msg;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildReplyMarkdown(opts) {
|
|
149
|
+
var previewURL = opts.previewURL || "";
|
|
150
|
+
var downloadURL = opts.downloadURL || "";
|
|
151
|
+
var deliverableType = opts.type || "";
|
|
152
|
+
var isDirectory = !!opts.isDirectory;
|
|
153
|
+
var lines = [];
|
|
154
|
+
if (previewURL) {
|
|
155
|
+
lines.push("预览链接:[点击预览](" + previewURL + ")");
|
|
156
|
+
}
|
|
157
|
+
if (downloadURL) {
|
|
158
|
+
if (isDirectory || deliverableType === "game") {
|
|
159
|
+
lines.push("文件列表:[查看目录](" + downloadURL + ")");
|
|
160
|
+
} else {
|
|
161
|
+
lines.push("下载链接:[点击下载](" + downloadURL + ")");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (lines.length === 0 && !previewURL && !downloadURL) {
|
|
165
|
+
return "交付物已上传成功。";
|
|
166
|
+
}
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Tool implementations ─────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function uploadDeliverable(args) {
|
|
173
|
+
var body = {
|
|
174
|
+
resourceId: args.resource_id,
|
|
175
|
+
groupId: args.group_id,
|
|
176
|
+
userId: args.user_id,
|
|
177
|
+
type: args.type,
|
|
178
|
+
fileName: args.file_name,
|
|
179
|
+
release: "" // overwritten below after release name derivation
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (args.files && args.files.length > 0) {
|
|
183
|
+
body.files = args.files.map(function(f) {
|
|
184
|
+
return {
|
|
185
|
+
name: f.name,
|
|
186
|
+
contentText: f.content_text || "",
|
|
187
|
+
contentBase64: f.content_base64 || ""
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
body.contentText = args.content_text || "";
|
|
192
|
+
body.contentBase64 = args.content_base64 || "";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Derive release name:
|
|
196
|
+
// 1) botID (runtime env, e.g. "user-xxx") is preferred
|
|
197
|
+
// 2) OPENCLAW_RELEASE / BOT_ID (compat)
|
|
198
|
+
// 3) fallback to stripping "oc-" prefix from OPENCLAW_GATEWAY_TOKEN
|
|
199
|
+
var release = process.env.botID || process.env.OPENCLAW_RELEASE || process.env.BOT_ID || "";
|
|
200
|
+
if (!release) {
|
|
201
|
+
var tok = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
202
|
+
if (tok.indexOf("oc-") === 0) release = tok.slice(3);
|
|
203
|
+
}
|
|
204
|
+
body.release = release;
|
|
205
|
+
|
|
206
|
+
return httpRequest("POST", "/openclaw-gateway/be/deliverables", body).then(function(resp) {
|
|
207
|
+
var d = resp.body.data || resp.body;
|
|
208
|
+
// Prefer backend-aware previewUrl returned by gateway (OSS/output/link).
|
|
209
|
+
// Fallback to legacy gateway preview endpoint for compatibility.
|
|
210
|
+
var previewURL = d.previewUrl || (GATEWAY_PUBLIC + "/openclaw-gateway/preview/" + d.uuid);
|
|
211
|
+
var isDirectory = !!(args.files && args.files.length > 0);
|
|
212
|
+
if (!isDirectory && d.downloadUrl && previewURL && d.downloadUrl !== previewURL && /(?:\?|&)list=1(?:&|$)/.test(d.downloadUrl)) {
|
|
213
|
+
isDirectory = true;
|
|
214
|
+
}
|
|
215
|
+
var replyMarkdown = buildReplyMarkdown({
|
|
216
|
+
previewURL: previewURL,
|
|
217
|
+
downloadURL: d.downloadUrl,
|
|
218
|
+
type: args.type,
|
|
219
|
+
isDirectory: isDirectory
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
uuid: d.uuid,
|
|
223
|
+
backend: d.backend || "",
|
|
224
|
+
download_url: d.downloadUrl,
|
|
225
|
+
preview_url: previewURL,
|
|
226
|
+
expire_at: d.expireAt,
|
|
227
|
+
reply_markdown: replyMarkdown,
|
|
228
|
+
trace_id: resp.traceID || "",
|
|
229
|
+
message: replyMarkdown
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── MCP JSON-RPC loop ────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
function callTool(name, args) {
|
|
237
|
+
switch (name) {
|
|
238
|
+
case "upload_deliverable": return uploadDeliverable(args);
|
|
239
|
+
default: return Promise.reject(new Error("unknown tool: " + name));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function send(obj) {
|
|
244
|
+
process.stdout.write(JSON.stringify(obj) + "\n");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function handleMessage(msg) {
|
|
248
|
+
var id = msg.id, method = msg.method, params = msg.params || {};
|
|
249
|
+
|
|
250
|
+
switch (method) {
|
|
251
|
+
case "initialize":
|
|
252
|
+
send({ jsonrpc: "2.0", id: id, result: {
|
|
253
|
+
protocolVersion: "2024-11-05",
|
|
254
|
+
capabilities: { tools: {} },
|
|
255
|
+
serverInfo: { name: "deliverables", version: "1.0.0" }
|
|
256
|
+
}});
|
|
257
|
+
return Promise.resolve();
|
|
258
|
+
|
|
259
|
+
case "notifications/initialized":
|
|
260
|
+
return Promise.resolve();
|
|
261
|
+
|
|
262
|
+
case "tools/list":
|
|
263
|
+
send({ jsonrpc: "2.0", id: id, result: { tools: TOOL_DEFS } });
|
|
264
|
+
return Promise.resolve();
|
|
265
|
+
|
|
266
|
+
case "tools/call":
|
|
267
|
+
return callTool(params.name, params.arguments || {}).then(
|
|
268
|
+
function(result) {
|
|
269
|
+
send({ jsonrpc: "2.0", id: id, result: {
|
|
270
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
271
|
+
}});
|
|
272
|
+
},
|
|
273
|
+
function(err) {
|
|
274
|
+
send({ jsonrpc: "2.0", id: id, result: {
|
|
275
|
+
content: [{ type: "text", text: "Error: " + err.message }],
|
|
276
|
+
isError: true
|
|
277
|
+
}});
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
default:
|
|
282
|
+
send({ jsonrpc: "2.0", id: id, error: { code: -32601, message: "Method not found: " + method } });
|
|
283
|
+
return Promise.resolve();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
var rl = require("readline").createInterface({ input: process.stdin, terminal: false });
|
|
288
|
+
rl.on("line", function(line) {
|
|
289
|
+
if (!line.trim()) return;
|
|
290
|
+
var msg;
|
|
291
|
+
try { msg = JSON.parse(line); } catch(e) { return; }
|
|
292
|
+
handleMessage(msg).catch(function(err) {
|
|
293
|
+
process.stderr.write("[deliverables] unhandled error: " + err.message + "\n");
|
|
294
|
+
});
|
|
295
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deliverables",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"npm_package": "@dai_ming/plugin-deliverables",
|
|
5
|
+
"description": "Deliverables plugin: MCP server + skill + AGENTS rules for AI-generated file uploads",
|
|
6
|
+
"mcp_servers": {
|
|
7
|
+
"deliverables": {
|
|
8
|
+
"script": "mcp-servers/deliverables.js",
|
|
9
|
+
"command": "node",
|
|
10
|
+
"env": {
|
|
11
|
+
"CLAW_GATEWAY_URL": "${CLAW_GATEWAY_URL}",
|
|
12
|
+
"CLAW_GATEWAY_PUBLIC_URL": "${CLAW_GATEWAY_PUBLIC_URL}",
|
|
13
|
+
"CLAW_GATEWAY_API_KEY": "${CLAW_GATEWAY_API_KEY}",
|
|
14
|
+
"OPENCLAW_GATEWAY_API_KEY": "${OPENCLAW_GATEWAY_API_KEY}"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"skills": {
|
|
19
|
+
"deliverables": "skills/deliverables/SKILL.md"
|
|
20
|
+
},
|
|
21
|
+
"agents_rules": {
|
|
22
|
+
"file": "agents-rules/deliverables.md",
|
|
23
|
+
"start_marker": "DELIVERABLE_LINK_RULES_START",
|
|
24
|
+
"end_marker": "DELIVERABLE_AUTO_UPLOAD_RULES_END"
|
|
25
|
+
},
|
|
26
|
+
"openclaw_config": {
|
|
27
|
+
"plugins": {
|
|
28
|
+
"entries": {
|
|
29
|
+
"deliverables": { "enabled": true }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dai_ming/plugin-deliverables",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openclaw",
|
|
7
|
+
"plugin",
|
|
8
|
+
"deliverables",
|
|
9
|
+
"mcp"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"files": [
|
|
13
|
+
"openclaw-plugin.json",
|
|
14
|
+
"mcp-servers/",
|
|
15
|
+
"skills/",
|
|
16
|
+
"agents-rules/"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=16"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deliverables
|
|
3
|
+
description: 上传AI生成的文件到交付物系统,返回可分享的预览链接
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 文件交付规则(强制)
|
|
7
|
+
|
|
8
|
+
当你生成了文件内容(文章、文档、介绍、攻略、HTML页面、Markdown、游戏、图片等),**必须**调用交付物上传工具,不得把文件内容直接粘贴在消息里。
|
|
9
|
+
|
|
10
|
+
## 工具名说明(强制)
|
|
11
|
+
|
|
12
|
+
- 始终调用当前会话里真实暴露出来的工具名。
|
|
13
|
+
- 当前运行时里交付物工具就是:
|
|
14
|
+
- `deliverables__upload_deliverable`
|
|
15
|
+
- 如果你在说明文字里同时看到了裸名 `upload_deliverable`,不要猜测;优先使用工具列表里实际出现的 namespaced 名称。
|
|
16
|
+
|
|
17
|
+
## 工作区文件路径(强制)
|
|
18
|
+
|
|
19
|
+
- 生成的文件默认写入当前 agent 工作区的 `output/` 子目录。
|
|
20
|
+
- 不要直接写到工作区根目录(如当前 workspace 根目录下的 `*.md`),应写到当前 workspace 的 `output/*.md`。
|
|
21
|
+
- 多文件内容(游戏/网站)写到 `output/<目录名>/...`,保持目录结构。
|
|
22
|
+
- 如果你使用 `write` 工具创建交付物文件,目标路径必须在 `output/` 下。
|
|
23
|
+
- 如果你已经误写到工作区根目录,必须先移动或复制到 `output/`,再继续上传交付物并向用户汇报。
|
|
24
|
+
|
|
25
|
+
## 调用前必须准备的参数(全部必填)
|
|
26
|
+
|
|
27
|
+
从当前会话上下文提取以下值,**调用时一次性填完,不要留空**:
|
|
28
|
+
|
|
29
|
+
| 参数 | 取值来源 | 示例 |
|
|
30
|
+
|------|---------|------|
|
|
31
|
+
| `resource_id` | 消息元数据中的 `resource_id` 字段 | `user_xxx_lobster_yyy` |
|
|
32
|
+
| `group_id` | 消息元数据中的 `group_id` 或 `conversation_id` | `group_abc123` |
|
|
33
|
+
| `user_id` | 消息元数据中的 `sender_id` 或 `owner_id` | `cbb0fab9...` |
|
|
34
|
+
| `type` | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`zip`/`link` | `article` |
|
|
35
|
+
| `file_name` | 有意义的文件名,含扩展名 | `report-2026.html` |
|
|
36
|
+
| `content_text` | 文件的完整文本内容(HTML/Markdown等) | `<html>...</html>` |
|
|
37
|
+
|
|
38
|
+
> **直接对话(direct chat)时**:`group_id` 填 `conversation_id`,`user_id` 填 `owner_id`。
|
|
39
|
+
|
|
40
|
+
## 多文件(游戏)
|
|
41
|
+
|
|
42
|
+
type 为 `game` 时,使用 `files` 数组替代 `content_text`:
|
|
43
|
+
```
|
|
44
|
+
files: [
|
|
45
|
+
{ name: "index.html", content_text: "..." },
|
|
46
|
+
{ name: "style.css", content_text: "..." }
|
|
47
|
+
]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
强制要求:
|
|
51
|
+
|
|
52
|
+
- 多文件网页/游戏默认按“目录”发布,不要先打 zip。
|
|
53
|
+
- 除非用户明确要求压缩包,否则必须优先用 `type=game` + `files`。
|
|
54
|
+
- 静态网页/游戏应尽量在根目录提供 `index.html`,这样预览链接可以直接打开主页。
|
|
55
|
+
- 如果内容需要启动独立后端服务、端口、数据库或长运行进程,交付物预览本身无法运行它;这类场景应改走部署/ingress,不要伪装成“可直接预览”的交付物。
|
|
56
|
+
|
|
57
|
+
## 上传成功后
|
|
58
|
+
|
|
59
|
+
优先规则:
|
|
60
|
+
- 最终消息必须先用你自己的话写 1-2 句简短介绍,粗略说明你生成了什么内容、包含哪些重点。
|
|
61
|
+
- 这句介绍必须基于实际产物内容,不要使用固定模板,例如:`交付物已上传成功,可直接在线预览或下载。`
|
|
62
|
+
- 如果工具结果里有 `reply_markdown`,把它原样放在这句介绍后面,不要改写其中的链接。
|
|
63
|
+
|
|
64
|
+
否则只发给用户:
|
|
65
|
+
- 预览链接(`preview_url`,必须用 Markdown 链接格式)
|
|
66
|
+
- 下载链接(`download_url`,必须用 Markdown 链接格式)
|
|
67
|
+
- 多文件/游戏目录场景下,第二行也可以是 `文件列表:[查看目录](...)`,不要改成裸 URL 或 zip 描述。
|
|
68
|
+
|
|
69
|
+
不要在消息里输出文件的完整内容、工作区保存路径或裸链接。
|