@dai_ming/plugin-deliverables 1.0.4 → 1.0.6
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 +79 -158
- package/install.js +463 -0
- package/openclaw-plugin.json +4 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,214 +1,135 @@
|
|
|
1
1
|
# @dai_ming/plugin-deliverables
|
|
2
2
|
|
|
3
|
-
OpenClaw
|
|
3
|
+
OpenClaw 交付物插件。安装后会把交付物 MCP、skill、AGENTS 规则和 `openclaw.json` 配置一次性落到运行目录里,让 Agent 默认把生成文件上传成可访问的交付物链接。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## 插件包含什么
|
|
5
|
+
## 包内文件
|
|
8
6
|
|
|
9
7
|
| 文件 | 作用 |
|
|
10
8
|
|------|------|
|
|
11
|
-
| `
|
|
12
|
-
| `
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
|
|
16
|
-
---
|
|
9
|
+
| `install.js` | 安装器:负责复制插件、注册 MCP、安装 skill、注入 AGENTS 规则、补齐 `plugins.allow`、清 session |
|
|
10
|
+
| `mcp-servers/deliverables.js` | MCP Server,暴露 `upload_deliverable` |
|
|
11
|
+
| `skills/deliverables/SKILL.md` | 约束模型优先走交付物上传,并统一写到 `output/` |
|
|
12
|
+
| `agents-rules/deliverables.md` | 注入到 `AGENTS.md` 的强约束规则 |
|
|
13
|
+
| `openclaw-plugin.json` | 插件清单,声明 MCP、skill、rules 和运行时配置 |
|
|
17
14
|
|
|
18
|
-
##
|
|
15
|
+
## 推荐安装方式
|
|
19
16
|
|
|
20
|
-
###
|
|
17
|
+
### 方式一:claw-gateway Helm 部署
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
把插件加入 `installPlugins`:
|
|
23
20
|
|
|
24
21
|
```yaml
|
|
25
|
-
# values.yaml(或 claw-gateway 管理界面的 Helm 参数)
|
|
26
22
|
installPlugins:
|
|
27
|
-
- "@dai_ming/plugin-deliverables@1.0.
|
|
23
|
+
- "@dai_ming/plugin-deliverables@1.0.6"
|
|
28
24
|
```
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
1. **Phase 3**:`npm pack @dai_ming/plugin-deliverables@1.0.4` 下载 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 自动部署)
|
|
26
|
+
现有 chart 会在 init 阶段完成安装。
|
|
47
27
|
|
|
48
|
-
|
|
28
|
+
### 方式二:npm 安装后执行安装脚本
|
|
49
29
|
|
|
50
|
-
|
|
30
|
+
这就是给运行中 pod / 自定义镜像准备的最小落地方式。
|
|
51
31
|
|
|
52
32
|
```bash
|
|
53
33
|
npm config set registry https://registry.npmmirror.com
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
tar xzf openclaw-plugin-deliverables-*.tgz -C ~/.openclaw/extensions-extra/plugin-deliverables --strip-components=1
|
|
34
|
+
npm install @dai_ming/plugin-deliverables@1.0.6
|
|
35
|
+
node node_modules/@dai_ming/plugin-deliverables/install.js
|
|
57
36
|
```
|
|
58
37
|
|
|
59
|
-
|
|
38
|
+
如果 OpenClaw home 不在默认位置,可以显式传入:
|
|
60
39
|
|
|
61
40
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
}
|
|
41
|
+
node node_modules/@dai_ming/plugin-deliverables/install.js \
|
|
42
|
+
--home /home/node/.openclaw
|
|
123
43
|
```
|
|
124
44
|
|
|
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/` 目录 |
|
|
45
|
+
## `install.js` 会做什么
|
|
135
46
|
|
|
136
|
-
|
|
47
|
+
执行一次脚本后,会自动完成这些动作:
|
|
137
48
|
|
|
138
|
-
|
|
49
|
+
1. 复制插件到:
|
|
50
|
+
- `~/.openclaw/extensions/plugin-deliverables`
|
|
51
|
+
- `~/.openclaw/extensions-extra/plugin-deliverables`
|
|
52
|
+
2. 复制 `deliverables.js` 到 `~/.openclaw/mcp-servers/`
|
|
53
|
+
3. 把 `SKILL.md` 写入所有已发现 workspace 和 agent skill 目录
|
|
54
|
+
4. 把交付物规则幂等注入到实际使用中的 `AGENTS.md`
|
|
55
|
+
5. 在各 workspace 下补出 `output/` 目录
|
|
56
|
+
6. 更新 `~/.openclaw/openclaw.json`
|
|
57
|
+
- 注册 `mcp.servers.deliverables`
|
|
58
|
+
- 启用 `skills.entries.deliverables`
|
|
59
|
+
- 启用 `plugins.entries.plugin-deliverables`
|
|
60
|
+
- 把 `plugin-deliverables` 加入 `plugins.allow`
|
|
61
|
+
7. 清理 session,让下一条消息重新读取 prompt / skill / tool 配置
|
|
62
|
+
8. 如果 agent 或全局存在 `tools.allow` 白名单,自动补上 `deliverables__upload_deliverable`
|
|
139
63
|
|
|
140
|
-
##
|
|
64
|
+
## 是否需要重启 Pod
|
|
141
65
|
|
|
142
|
-
|
|
66
|
+
不需要重启 Pod。
|
|
143
67
|
|
|
144
|
-
|
|
145
|
-
-
|
|
146
|
-
-
|
|
147
|
-
-
|
|
68
|
+
原因:
|
|
69
|
+
- 安装脚本会直接改 `~/.openclaw/openclaw.json`
|
|
70
|
+
- `plugins.allow` 变化会触发 OpenClaw 的 watcher,进程级重载大约 15 秒
|
|
71
|
+
- session 也会被清掉,所以下一条消息会重新读取最新的 `AGENTS.md` / skill
|
|
148
72
|
|
|
149
|
-
|
|
73
|
+
建议做法:
|
|
150
74
|
|
|
151
|
-
|
|
75
|
+
```bash
|
|
76
|
+
node node_modules/@dai_ming/plugin-deliverables/install.js
|
|
77
|
+
sleep 20
|
|
78
|
+
```
|
|
152
79
|
|
|
153
|
-
|
|
80
|
+
然后再发一条新的用户消息测试。
|
|
154
81
|
|
|
155
|
-
|
|
82
|
+
## 常用参数
|
|
156
83
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 | — | 多文件列表(游戏/站点场景,优先使用) |
|
|
84
|
+
```bash
|
|
85
|
+
node install.js --help
|
|
86
|
+
```
|
|
167
87
|
|
|
168
|
-
|
|
88
|
+
| 参数 | 说明 |
|
|
89
|
+
|------|------|
|
|
90
|
+
| `--home <dir>` | 指定 OpenClaw home,默认 `~/.openclaw` |
|
|
91
|
+
| `--plugin-root <dir>` | 指定插件源码目录,默认当前包目录 |
|
|
92
|
+
| `--no-clear-sessions` | 不清 session |
|
|
93
|
+
| `--dry-run` | 只打印计划,不落盘 |
|
|
169
94
|
|
|
170
|
-
|
|
95
|
+
## 返回结果
|
|
171
96
|
|
|
172
|
-
|
|
97
|
+
安装脚本成功后会输出一段 JSON 摘要,包含:
|
|
173
98
|
|
|
174
|
-
|
|
99
|
+
- 目标 OpenClaw home
|
|
100
|
+
- 写入的 extension 目录
|
|
101
|
+
- 写入的 MCP 文件
|
|
102
|
+
- 修改的 `AGENTS.md`
|
|
103
|
+
- 清理的 session 数量
|
|
175
104
|
|
|
176
|
-
|
|
105
|
+
## 发布
|
|
177
106
|
|
|
178
107
|
```bash
|
|
179
|
-
# 1. 修改 packages/plugin-deliverables/package.json 中的 version
|
|
180
|
-
# 2. 同步更新 packages/plugin-deliverables/mcp-servers/deliverables.js(如有变更)
|
|
181
|
-
# 3. 打包并发布到 npmmirror 兼容的私有或公开 registry
|
|
182
108
|
cd packages/plugin-deliverables
|
|
183
109
|
npm publish --registry https://registry.npmjs.org --access public
|
|
184
110
|
```
|
|
185
111
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
| claw-gateway 版本 | plugin 版本 |
|
|
189
|
-
|-------------------|-------------|
|
|
190
|
-
| 当前 | 1.0.4 |
|
|
112
|
+
## 排障
|
|
191
113
|
|
|
192
|
-
|
|
114
|
+
### Agent 没看到工具
|
|
193
115
|
|
|
194
|
-
|
|
116
|
+
重点检查:
|
|
195
117
|
|
|
196
|
-
|
|
118
|
+
1. `~/.openclaw/openclaw.json` 里是否有 `mcp.servers.deliverables`
|
|
119
|
+
2. `plugins.allow` 是否含 `plugin-deliverables`
|
|
120
|
+
3. `~/.openclaw/mcp-servers/deliverables.js` 是否存在
|
|
121
|
+
4. 当前 workspace 的 `AGENTS.md` 是否已经注入 deliverables 规则
|
|
197
122
|
|
|
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`
|
|
123
|
+
### 上传返回 401 / 404
|
|
202
124
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
确认 `CLAW_GATEWAY_URL` 和 `CLAW_GATEWAY_API_KEY` 注入正确,可在容器内测试:
|
|
125
|
+
优先检查容器环境变量:
|
|
206
126
|
|
|
207
127
|
```bash
|
|
208
|
-
|
|
128
|
+
env | grep -E 'CLAW_GATEWAY|OPENCLAW_GATEWAY|botID'
|
|
209
129
|
```
|
|
210
130
|
|
|
211
|
-
|
|
131
|
+
以及直接请求 gateway:
|
|
212
132
|
|
|
213
|
-
|
|
214
|
-
|
|
133
|
+
```bash
|
|
134
|
+
curl -H "X-API-Key: $CLAW_GATEWAY_API_KEY" "$CLAW_GATEWAY_URL/healthz"
|
|
135
|
+
```
|
package/install.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
var fs = require("fs");
|
|
5
|
+
var os = require("os");
|
|
6
|
+
var path = require("path");
|
|
7
|
+
|
|
8
|
+
var LEGACY_DELIVERABLE_TOOL_NAMES = {
|
|
9
|
+
"upload_deliverable": true,
|
|
10
|
+
"list_deliverables": true,
|
|
11
|
+
"renew_deliverable": true,
|
|
12
|
+
"deliverables__upload_deliverable": true,
|
|
13
|
+
"deliverables__list_deliverables": true,
|
|
14
|
+
"deliverables__renew_deliverable": true
|
|
15
|
+
};
|
|
16
|
+
var DELIVERABLES_UPLOAD_TOOL = "deliverables__upload_deliverable";
|
|
17
|
+
|
|
18
|
+
function usage() {
|
|
19
|
+
console.log([
|
|
20
|
+
"Usage:",
|
|
21
|
+
" node install.js [--home <openclaw-home>] [--plugin-root <dir>] [--no-clear-sessions] [--dry-run]",
|
|
22
|
+
"",
|
|
23
|
+
"Defaults:",
|
|
24
|
+
" --home " + path.join(os.homedir(), ".openclaw"),
|
|
25
|
+
" --plugin-root current package directory"
|
|
26
|
+
].join("\n"));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
var args = {
|
|
31
|
+
home: path.join(os.homedir(), ".openclaw"),
|
|
32
|
+
pluginRoot: __dirname,
|
|
33
|
+
clearSessions: true,
|
|
34
|
+
dryRun: false
|
|
35
|
+
};
|
|
36
|
+
for (var i = 0; i < argv.length; i++) {
|
|
37
|
+
var cur = argv[i];
|
|
38
|
+
if (cur === "--home") {
|
|
39
|
+
i += 1;
|
|
40
|
+
if (i >= argv.length) throw new Error("--home requires a value");
|
|
41
|
+
args.home = path.resolve(argv[i]);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (cur === "--plugin-root") {
|
|
45
|
+
i += 1;
|
|
46
|
+
if (i >= argv.length) throw new Error("--plugin-root requires a value");
|
|
47
|
+
args.pluginRoot = path.resolve(argv[i]);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (cur === "--no-clear-sessions") {
|
|
51
|
+
args.clearSessions = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (cur === "--dry-run") {
|
|
55
|
+
args.dryRun = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (cur === "--help" || cur === "-h") {
|
|
59
|
+
usage();
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
throw new Error("unknown argument: " + cur);
|
|
63
|
+
}
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isPlainObject(val) {
|
|
68
|
+
return !!val && typeof val === "object" && !Array.isArray(val);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function deepCopy(val) {
|
|
72
|
+
if (val === undefined) return undefined;
|
|
73
|
+
return JSON.parse(JSON.stringify(val));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveEnv(val) {
|
|
77
|
+
if (typeof val !== "string") return val;
|
|
78
|
+
return val.replace(/\$\{([^}]+)\}/g, function(_, key) {
|
|
79
|
+
return process.env[key] || "";
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveEnvDeep(val) {
|
|
84
|
+
if (Array.isArray(val)) return val.map(resolveEnvDeep);
|
|
85
|
+
if (isPlainObject(val)) {
|
|
86
|
+
var out = {};
|
|
87
|
+
Object.keys(val).forEach(function(key) {
|
|
88
|
+
out[key] = resolveEnvDeep(val[key]);
|
|
89
|
+
});
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
return resolveEnv(val);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeRuntimeName(name) {
|
|
96
|
+
if (typeof name !== "string") return "";
|
|
97
|
+
var out = name.trim();
|
|
98
|
+
if (!out) return "";
|
|
99
|
+
if (out.charAt(0) === "@") {
|
|
100
|
+
var slash = out.indexOf("/");
|
|
101
|
+
out = slash >= 0 ? out.slice(slash + 1) : out.slice(1);
|
|
102
|
+
}
|
|
103
|
+
var at = out.indexOf("@");
|
|
104
|
+
if (at >= 0) out = out.slice(0, at);
|
|
105
|
+
return out.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function ensureDir(dir, dryRun) {
|
|
109
|
+
if (dryRun) return;
|
|
110
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readJSON(filePath, fallback) {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
116
|
+
} catch (err) {
|
|
117
|
+
return deepCopy(fallback);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeJSON(filePath, value, dryRun) {
|
|
122
|
+
if (dryRun) return;
|
|
123
|
+
ensureDir(path.dirname(filePath), dryRun);
|
|
124
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function samePath(a, b) {
|
|
128
|
+
try {
|
|
129
|
+
return fs.realpathSync(a) === fs.realpathSync(b);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function copyRecursive(src, dst, dryRun) {
|
|
136
|
+
var stat = fs.lstatSync(src);
|
|
137
|
+
if (stat.isSymbolicLink()) {
|
|
138
|
+
var target = fs.realpathSync(src);
|
|
139
|
+
copyRecursive(target, dst, dryRun);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (stat.isDirectory()) {
|
|
143
|
+
ensureDir(dst, dryRun);
|
|
144
|
+
fs.readdirSync(src).forEach(function(entry) {
|
|
145
|
+
if (entry === "node_modules" || entry === ".git") return;
|
|
146
|
+
copyRecursive(path.join(src, entry), path.join(dst, entry), dryRun);
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
ensureDir(path.dirname(dst), dryRun);
|
|
151
|
+
if (!dryRun) {
|
|
152
|
+
fs.copyFileSync(src, dst);
|
|
153
|
+
if (path.extname(dst) === ".js") {
|
|
154
|
+
fs.chmodSync(dst, 0o755);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function replaceDirectory(src, dst, dryRun) {
|
|
160
|
+
if (samePath(src, dst)) return false;
|
|
161
|
+
if (!dryRun) fs.rmSync(dst, { recursive: true, force: true });
|
|
162
|
+
copyRecursive(src, dst, dryRun);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizePluginEntryMap(entriesCfg, pluginId) {
|
|
167
|
+
if (!isPlainObject(entriesCfg)) return {};
|
|
168
|
+
var merged = {};
|
|
169
|
+
Object.keys(entriesCfg).forEach(function(name) {
|
|
170
|
+
var item = entriesCfg[name];
|
|
171
|
+
if (!isPlainObject(item)) return;
|
|
172
|
+
Object.keys(item).forEach(function(key) {
|
|
173
|
+
merged[key] = deepCopy(item[key]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
if (merged.enabled === undefined) merged.enabled = true;
|
|
177
|
+
var out = {};
|
|
178
|
+
out[pluginId] = merged;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function collectWorkspaceRoots(home, cfg) {
|
|
183
|
+
var roots = {};
|
|
184
|
+
function add(dir) {
|
|
185
|
+
if (!dir || typeof dir !== "string") return;
|
|
186
|
+
roots[path.resolve(dir)] = true;
|
|
187
|
+
}
|
|
188
|
+
add(path.join(home, "workspace"));
|
|
189
|
+
try {
|
|
190
|
+
fs.readdirSync(home).forEach(function(entry) {
|
|
191
|
+
if (entry === "workspace" || entry.indexOf("workspace-") === 0) {
|
|
192
|
+
add(path.join(home, entry));
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
} catch (err) {}
|
|
196
|
+
if (cfg && cfg.agents && Array.isArray(cfg.agents.list)) {
|
|
197
|
+
cfg.agents.list.forEach(function(agent) {
|
|
198
|
+
if (agent && typeof agent.workspace === "string" && agent.workspace.trim()) {
|
|
199
|
+
add(agent.workspace.trim());
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return Object.keys(roots);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function collectAgentIDs(home, cfg) {
|
|
207
|
+
var ids = {};
|
|
208
|
+
function add(id) {
|
|
209
|
+
if (!id || typeof id !== "string") return;
|
|
210
|
+
ids[id.trim()] = true;
|
|
211
|
+
}
|
|
212
|
+
add("main");
|
|
213
|
+
try {
|
|
214
|
+
fs.readdirSync(path.join(home, "agents")).forEach(function(entry) {
|
|
215
|
+
if (entry && entry !== "." && entry !== "..") add(entry);
|
|
216
|
+
});
|
|
217
|
+
} catch (err) {}
|
|
218
|
+
if (cfg && cfg.agents && Array.isArray(cfg.agents.list)) {
|
|
219
|
+
cfg.agents.list.forEach(function(agent) {
|
|
220
|
+
if (agent && agent.id) add(String(agent.id));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return Object.keys(ids);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function writeText(filePath, content, dryRun) {
|
|
227
|
+
if (dryRun) return;
|
|
228
|
+
ensureDir(path.dirname(filePath), dryRun);
|
|
229
|
+
fs.writeFileSync(filePath, content);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function installSkills(pluginRoot, manifest, home, workspaceRoots, agentIDs, dryRun) {
|
|
233
|
+
var installed = [];
|
|
234
|
+
if (!isPlainObject(manifest.skills)) return installed;
|
|
235
|
+
Object.keys(manifest.skills).forEach(function(skillName) {
|
|
236
|
+
var relFile = manifest.skills[skillName];
|
|
237
|
+
var src = path.join(pluginRoot, relFile);
|
|
238
|
+
var content = fs.readFileSync(src, "utf8");
|
|
239
|
+
workspaceRoots.forEach(function(wsRoot) {
|
|
240
|
+
var skillFile = path.join(wsRoot, "skills", skillName, "SKILL.md");
|
|
241
|
+
writeText(skillFile, content, dryRun);
|
|
242
|
+
ensureDir(path.join(wsRoot, "output"), dryRun);
|
|
243
|
+
installed.push(skillFile);
|
|
244
|
+
});
|
|
245
|
+
agentIDs.forEach(function(agentID) {
|
|
246
|
+
var skillFile = path.join(home, "agents", agentID, "skills", skillName, "SKILL.md");
|
|
247
|
+
writeText(skillFile, content, dryRun);
|
|
248
|
+
installed.push(skillFile);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
return installed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function injectRules(pluginRoot, manifest, workspaceRoots, dryRun) {
|
|
255
|
+
if (!manifest.agents_rules || !manifest.agents_rules.file) return [];
|
|
256
|
+
var startMarker = manifest.agents_rules.start_marker || "";
|
|
257
|
+
var endMarker = manifest.agents_rules.end_marker || "";
|
|
258
|
+
var rules = fs.readFileSync(path.join(pluginRoot, manifest.agents_rules.file), "utf8").trim() + "\n";
|
|
259
|
+
var changed = [];
|
|
260
|
+
workspaceRoots.forEach(function(wsRoot) {
|
|
261
|
+
var agentsFile = path.join(wsRoot, "AGENTS.md");
|
|
262
|
+
var existing = "";
|
|
263
|
+
try {
|
|
264
|
+
existing = fs.readFileSync(agentsFile, "utf8");
|
|
265
|
+
} catch (err) {}
|
|
266
|
+
var next = existing;
|
|
267
|
+
if (startMarker && endMarker && existing.indexOf(startMarker) >= 0) {
|
|
268
|
+
var re = new RegExp("<!--\\s*" + escapeRegExp(startMarker) + "\\s*-->[\\s\\S]*?<!--\\s*" + escapeRegExp(endMarker) + "\\s*-->", "g");
|
|
269
|
+
next = existing.replace(re, rules.trim());
|
|
270
|
+
if (next !== "" && next.charAt(next.length - 1) !== "\n") next += "\n";
|
|
271
|
+
} else if (existing) {
|
|
272
|
+
next = existing.replace(/\s*$/, "") + "\n\n" + rules;
|
|
273
|
+
} else {
|
|
274
|
+
next = rules;
|
|
275
|
+
}
|
|
276
|
+
ensureDir(path.join(wsRoot, "output"), dryRun);
|
|
277
|
+
if (next !== existing) {
|
|
278
|
+
writeText(agentsFile, next, dryRun);
|
|
279
|
+
changed.push(agentsFile);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
return changed;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function escapeRegExp(text) {
|
|
286
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sanitizeToolList(items, isAllow) {
|
|
290
|
+
if (!Array.isArray(items)) return items;
|
|
291
|
+
var out = [];
|
|
292
|
+
var seen = {};
|
|
293
|
+
var hasWildcard = false;
|
|
294
|
+
items.forEach(function(raw) {
|
|
295
|
+
var item = String(raw || "").trim();
|
|
296
|
+
if (!item) return;
|
|
297
|
+
if (item === "*") hasWildcard = true;
|
|
298
|
+
if (LEGACY_DELIVERABLE_TOOL_NAMES[item]) return;
|
|
299
|
+
if (!seen[item]) {
|
|
300
|
+
out.push(item);
|
|
301
|
+
seen[item] = true;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
if (isAllow && !hasWildcard && !seen[DELIVERABLES_UPLOAD_TOOL]) {
|
|
305
|
+
out.push(DELIVERABLES_UPLOAD_TOOL);
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function sanitizeToolConfig(toolsCfg) {
|
|
311
|
+
if (!isPlainObject(toolsCfg)) return;
|
|
312
|
+
if (Array.isArray(toolsCfg.allow)) toolsCfg.allow = sanitizeToolList(toolsCfg.allow, true);
|
|
313
|
+
if (Array.isArray(toolsCfg.deny)) toolsCfg.deny = sanitizeToolList(toolsCfg.deny, false);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function applyPluginConfig(cfg, manifest, home) {
|
|
317
|
+
if (!isPlainObject(cfg)) cfg = {};
|
|
318
|
+
var pluginId = normalizeRuntimeName(manifest.name || "plugin-deliverables");
|
|
319
|
+
var mcpRoot = path.join(home, "mcp-servers");
|
|
320
|
+
|
|
321
|
+
cfg.plugins = isPlainObject(cfg.plugins) ? cfg.plugins : {};
|
|
322
|
+
cfg.plugins.allow = Array.isArray(cfg.plugins.allow) ? cfg.plugins.allow.slice() : [];
|
|
323
|
+
if (cfg.plugins.allow.indexOf(pluginId) === -1) cfg.plugins.allow.push(pluginId);
|
|
324
|
+
cfg.plugins.entries = isPlainObject(cfg.plugins.entries) ? cfg.plugins.entries : {};
|
|
325
|
+
|
|
326
|
+
var manifestPluginCfg = isPlainObject(manifest.openclaw_config) ? resolveEnvDeep(manifest.openclaw_config) : {};
|
|
327
|
+
var desiredEntries = {};
|
|
328
|
+
if (isPlainObject(manifestPluginCfg.plugins) && isPlainObject(manifestPluginCfg.plugins.entries)) {
|
|
329
|
+
desiredEntries = normalizePluginEntryMap(manifestPluginCfg.plugins.entries, pluginId);
|
|
330
|
+
}
|
|
331
|
+
var desiredPluginEntry = desiredEntries[pluginId] || { enabled: true };
|
|
332
|
+
if (desiredPluginEntry.enabled === undefined) desiredPluginEntry.enabled = true;
|
|
333
|
+
var currentEntry = isPlainObject(cfg.plugins.entries[pluginId]) ? cfg.plugins.entries[pluginId] : {};
|
|
334
|
+
cfg.plugins.entries[pluginId] = Object.assign({}, currentEntry, desiredPluginEntry);
|
|
335
|
+
|
|
336
|
+
cfg.skills = isPlainObject(cfg.skills) ? cfg.skills : {};
|
|
337
|
+
cfg.skills.entries = isPlainObject(cfg.skills.entries) ? cfg.skills.entries : {};
|
|
338
|
+
if (isPlainObject(manifest.skills)) {
|
|
339
|
+
Object.keys(manifest.skills).forEach(function(skillName) {
|
|
340
|
+
var currentSkill = isPlainObject(cfg.skills.entries[skillName]) ? cfg.skills.entries[skillName] : {};
|
|
341
|
+
cfg.skills.entries[skillName] = Object.assign({}, currentSkill, { enabled: true });
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
cfg.mcp = isPlainObject(cfg.mcp) ? cfg.mcp : {};
|
|
346
|
+
cfg.mcp.servers = isPlainObject(cfg.mcp.servers) ? cfg.mcp.servers : {};
|
|
347
|
+
if (isPlainObject(manifest.mcp_servers)) {
|
|
348
|
+
Object.keys(manifest.mcp_servers).forEach(function(serverName) {
|
|
349
|
+
var server = manifest.mcp_servers[serverName];
|
|
350
|
+
if (!isPlainObject(server)) return;
|
|
351
|
+
var generated = {
|
|
352
|
+
command: typeof server.command === "string" && server.command.trim() ? resolveEnv(server.command) : "node"
|
|
353
|
+
};
|
|
354
|
+
if (Array.isArray(server.args) && server.args.length) {
|
|
355
|
+
generated.args = resolveEnvDeep(server.args);
|
|
356
|
+
} else if (typeof server.script === "string" && server.script.trim()) {
|
|
357
|
+
generated.args = [path.join(mcpRoot, path.basename(server.script))];
|
|
358
|
+
}
|
|
359
|
+
if (isPlainObject(server.env)) generated.env = resolveEnvDeep(server.env);
|
|
360
|
+
if (typeof server.cwd === "string" && server.cwd.trim()) generated.cwd = resolveEnv(server.cwd);
|
|
361
|
+
if (typeof server.transport === "string" && server.transport.trim()) generated.transport = resolveEnv(server.transport);
|
|
362
|
+
var currentServer = isPlainObject(cfg.mcp.servers[serverName]) ? cfg.mcp.servers[serverName] : {};
|
|
363
|
+
cfg.mcp.servers[serverName] = Object.assign({}, currentServer, generated);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
sanitizeToolConfig(cfg.tools);
|
|
368
|
+
if (cfg.agents && Array.isArray(cfg.agents.list)) {
|
|
369
|
+
cfg.agents.list.forEach(function(agent) {
|
|
370
|
+
if (isPlainObject(agent)) sanitizeToolConfig(agent.tools);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return cfg;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function installMcpServers(pluginRoot, manifest, home, dryRun) {
|
|
378
|
+
var copied = [];
|
|
379
|
+
if (!isPlainObject(manifest.mcp_servers)) return copied;
|
|
380
|
+
Object.keys(manifest.mcp_servers).forEach(function(serverName) {
|
|
381
|
+
var server = manifest.mcp_servers[serverName];
|
|
382
|
+
if (!isPlainObject(server) || typeof server.script !== "string" || !server.script.trim()) return;
|
|
383
|
+
var src = path.join(pluginRoot, server.script);
|
|
384
|
+
var dst = path.join(home, "mcp-servers", path.basename(server.script));
|
|
385
|
+
ensureDir(path.dirname(dst), dryRun);
|
|
386
|
+
if (!dryRun) {
|
|
387
|
+
fs.copyFileSync(src, dst);
|
|
388
|
+
fs.chmodSync(dst, 0o755);
|
|
389
|
+
}
|
|
390
|
+
copied.push(dst);
|
|
391
|
+
});
|
|
392
|
+
return copied;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function clearSessions(home, dryRun) {
|
|
396
|
+
var cleared = [];
|
|
397
|
+
var agentsRoot = path.join(home, "agents");
|
|
398
|
+
try {
|
|
399
|
+
fs.readdirSync(agentsRoot).forEach(function(agentID) {
|
|
400
|
+
var sessionDir = path.join(agentsRoot, agentID, "sessions");
|
|
401
|
+
if (!fs.existsSync(sessionDir)) return;
|
|
402
|
+
fs.readdirSync(sessionDir).forEach(function(entry) {
|
|
403
|
+
if (!/\.jsonl$/.test(entry) && entry !== "sessions.json") return;
|
|
404
|
+
var full = path.join(sessionDir, entry);
|
|
405
|
+
if (!dryRun) fs.rmSync(full, { force: true });
|
|
406
|
+
cleared.push(full);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
} catch (err) {}
|
|
410
|
+
return cleared;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function main() {
|
|
414
|
+
var args = parseArgs(process.argv.slice(2));
|
|
415
|
+
var pluginRoot = path.resolve(args.pluginRoot);
|
|
416
|
+
var home = path.resolve(args.home);
|
|
417
|
+
var manifestPath = path.join(pluginRoot, "openclaw-plugin.json");
|
|
418
|
+
if (!fs.existsSync(manifestPath)) {
|
|
419
|
+
throw new Error("missing openclaw-plugin.json at " + manifestPath);
|
|
420
|
+
}
|
|
421
|
+
var manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
422
|
+
var pluginName = normalizeRuntimeName(manifest.name || "plugin-deliverables");
|
|
423
|
+
var openclawPath = path.join(home, "openclaw.json");
|
|
424
|
+
var currentConfig = readJSON(openclawPath, {});
|
|
425
|
+
var workspaceRoots = collectWorkspaceRoots(home, currentConfig);
|
|
426
|
+
var agentIDs = collectAgentIDs(home, currentConfig);
|
|
427
|
+
var extensionsDir = path.join(home, "extensions", pluginName);
|
|
428
|
+
var extensionsExtraDir = path.join(home, "extensions-extra", pluginName);
|
|
429
|
+
|
|
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.");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
main();
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.error("[plugin-deliverables] install failed:", err && err.stack ? err.stack : err);
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
package/openclaw-plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plugin-deliverables",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
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": {
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
"openclaw_config": {
|
|
27
27
|
"plugins": {
|
|
28
28
|
"entries": {
|
|
29
|
-
"plugin-deliverables": {
|
|
29
|
+
"plugin-deliverables": {
|
|
30
|
+
"enabled": true
|
|
31
|
+
}
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dai_ming/plugin-deliverables",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
],
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"files": [
|
|
13
|
+
"install.js",
|
|
13
14
|
"openclaw-plugin.json",
|
|
14
15
|
"mcp-servers/",
|
|
15
16
|
"skills/",
|