@dai_ming/plugin-deliverables 1.0.15 → 1.0.16
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/INSTALL.md +7 -6
- package/README.md +6 -4
- package/index.js +252 -0
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +1 -1
package/INSTALL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# plugin-deliverables 安装文档
|
|
2
2
|
|
|
3
|
-
本文档描述 `@dai_ming/plugin-deliverables@1.0.
|
|
3
|
+
本文档描述 `@dai_ming/plugin-deliverables@1.0.16` 的安装、升级与验证方式。
|
|
4
4
|
|
|
5
5
|
## 1. 目标
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
适用于已经支持 `openclaw plugins install` 的运行环境。
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
openclaw plugins install @dai_ming/plugin-deliverables@1.0.
|
|
25
|
+
openclaw plugins install @dai_ming/plugin-deliverables@1.0.16 --pin
|
|
26
26
|
openclaw plugins enable plugin-deliverables
|
|
27
27
|
```
|
|
28
28
|
|
|
@@ -47,7 +47,7 @@ openclaw plugins list
|
|
|
47
47
|
|
|
48
48
|
```yaml
|
|
49
49
|
installPlugins:
|
|
50
|
-
- "@dai_ming/plugin-deliverables@1.0.
|
|
50
|
+
- "@dai_ming/plugin-deliverables@1.0.16"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Helm 初始化时会自动完成:
|
|
@@ -66,8 +66,8 @@ Helm 初始化时会自动完成:
|
|
|
66
66
|
```bash
|
|
67
67
|
mkdir -p /home/node/.openclaw/extensions-extra/plugin-deliverables
|
|
68
68
|
cd /tmp
|
|
69
|
-
npm pack @dai_ming/plugin-deliverables@1.0.
|
|
70
|
-
tar xzf dai_ming-plugin-deliverables-1.0.
|
|
69
|
+
npm pack @dai_ming/plugin-deliverables@1.0.16 --registry https://registry.npmjs.org
|
|
70
|
+
tar xzf dai_ming-plugin-deliverables-1.0.16.tgz \
|
|
71
71
|
-C /home/node/.openclaw/extensions-extra/plugin-deliverables \
|
|
72
72
|
--strip-components=1
|
|
73
73
|
```
|
|
@@ -174,6 +174,7 @@ ls -la /home/node/.openclaw/workspace/skills/deliverables
|
|
|
174
174
|
1. 第一条回复:模型自己的简短内容介绍
|
|
175
175
|
2. 第二条回复:纯一个 https 链接
|
|
176
176
|
3. 不再出现 `message + file_url/media` 的旁路发送
|
|
177
|
+
4. pod stdout 中会多出 `palz summary request/response` 与 `palz links request/response` 四条插件日志
|
|
177
178
|
|
|
178
179
|
## 5. 常见问题
|
|
179
180
|
|
|
@@ -211,5 +212,5 @@ cat /home/node/.openclaw/extensions-extra/plugin-deliverables/package.json
|
|
|
211
212
|
必须是:
|
|
212
213
|
|
|
213
214
|
```json
|
|
214
|
-
{ "version": "1.0.
|
|
215
|
+
{ "version": "1.0.16" }
|
|
215
216
|
```
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ OpenClaw 交付物插件 — 让 AI Agent 将生成的文件(文章、HTML 页
|
|
|
24
24
|
现在这个包同时支持 OpenClaw 原生插件加载。也就是说,除了网关侧用 `openclaw-plugin.json` 注入 MCP/skill/AGENTS 规则外,OpenClaw 还会读取 `openclaw.plugin.json` + `index.js`,把运行时 hook 也一并启用。
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
openclaw plugins install @dai_ming/plugin-deliverables@1.0.
|
|
27
|
+
openclaw plugins install @dai_ming/plugin-deliverables@1.0.16 --pin
|
|
28
28
|
openclaw plugins enable plugin-deliverables
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -43,11 +43,11 @@ openclaw plugins enable plugin-deliverables
|
|
|
43
43
|
```yaml
|
|
44
44
|
# values.yaml(或 claw-gateway 管理界面的 Helm 参数)
|
|
45
45
|
installPlugins:
|
|
46
|
-
- "@dai_ming/plugin-deliverables@1.0.
|
|
46
|
+
- "@dai_ming/plugin-deliverables@1.0.16"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
initContainer 执行顺序:
|
|
50
|
-
1. **Phase 3**:`npm pack @dai_ming/plugin-deliverables@1.0.
|
|
50
|
+
1. **Phase 3**:`npm pack @dai_ming/plugin-deliverables@1.0.16` 下载 tarball → 解压到 `/data/extensions-extra/plugin-deliverables/`
|
|
51
51
|
2. **Phase 3b**:在插件目录执行 `npm install --omit=dev`(本插件无运行时依赖,此步骤跳过)
|
|
52
52
|
3. **Phase 3e**:读取 `openclaw-plugin.json` 清单,自动:
|
|
53
53
|
- 复制 `mcp-servers/deliverables.js` → `/data/mcp-servers/deliverables.js`
|
|
@@ -201,6 +201,8 @@ fi
|
|
|
201
201
|
|
|
202
202
|
这三层一起工作的目标是:即使 prompt 漂移,也尽量把“message + file/media”这条旁路堵住,统一收敛到 `deliverables__upload_deliverable`。
|
|
203
203
|
|
|
204
|
+
另外,Palz 渠道下如果交付物回复会被拆成“内容简介 + 最终链接”两条消息,runtime plugin 还会分别打印这两次真实 HTTP 发送的 request/response 日志,便于直接从 pod stdout 排查。
|
|
205
|
+
|
|
204
206
|
---
|
|
205
207
|
|
|
206
208
|
## 版本与发布
|
|
@@ -221,7 +223,7 @@ npm publish --registry https://registry.npmjs.org --access public
|
|
|
221
223
|
|
|
222
224
|
| claw-gateway 版本 | plugin 版本 |
|
|
223
225
|
|-------------------|-------------|
|
|
224
|
-
| 当前 | 1.0.
|
|
226
|
+
| 当前 | 1.0.16 |
|
|
225
227
|
|
|
226
228
|
---
|
|
227
229
|
|
package/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
|
|
4
|
+
|
|
3
5
|
const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
4
6
|
"## Deliverables Runtime Guard (HARD CONSTRAINT)",
|
|
5
7
|
"",
|
|
@@ -10,6 +12,10 @@ const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
|
10
12
|
"- After a successful upload, reply with a short content-aware intro and then preserve the Markdown links returned by the deliverables tool.",
|
|
11
13
|
].join("\n");
|
|
12
14
|
|
|
15
|
+
function isString(value) {
|
|
16
|
+
return typeof value === "string";
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
const ATTACHMENT_PARAM_KEYS = new Set([
|
|
14
20
|
"attachment",
|
|
15
21
|
"attachments",
|
|
@@ -103,11 +109,257 @@ function extractMediaUrls(metadata) {
|
|
|
103
109
|
return urls;
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
function trimTrailingBlankLines(lines) {
|
|
113
|
+
const out = lines.slice();
|
|
114
|
+
while (out.length > 0 && /^\s*$/.test(out[out.length - 1])) {
|
|
115
|
+
out.pop();
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stripBulletPrefix(line) {
|
|
121
|
+
return String(line || "").replace(/^\s*[-*+]\s+/, "");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isLinksHeading(line) {
|
|
125
|
+
return /^\s*#{1,6}\s*访问链接\s*$/u.test(String(line || ""));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function extractDeliverableURL(line) {
|
|
129
|
+
const text = String(line || "").trim();
|
|
130
|
+
if (!text) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
|
|
134
|
+
if (markdownLink && markdownLink[1]) {
|
|
135
|
+
return markdownLink[1].trim();
|
|
136
|
+
}
|
|
137
|
+
const rawLink = text.match(/https?:\/\/\S+/iu);
|
|
138
|
+
if (rawLink && rawLink[0]) {
|
|
139
|
+
return rawLink[0].trim();
|
|
140
|
+
}
|
|
141
|
+
return "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isDeliverableLinkLine(line) {
|
|
145
|
+
const text = String(line || "");
|
|
146
|
+
if (
|
|
147
|
+
/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(
|
|
148
|
+
text,
|
|
149
|
+
)
|
|
150
|
+
) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
return !!extractDeliverableURL(text);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function splitDeliverableMessage(text) {
|
|
157
|
+
const normalized = String(text || "").replace(/\r\n/g, "\n").trim();
|
|
158
|
+
if (!normalized) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const lines = normalized.split("\n");
|
|
163
|
+
let firstLinkIndex = -1;
|
|
164
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
165
|
+
if (isDeliverableLinkLine(lines[i])) {
|
|
166
|
+
firstLinkIndex = i;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (firstLinkIndex < 0) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
|
|
175
|
+
if (summaryLines.length > 0 && isLinksHeading(summaryLines[summaryLines.length - 1])) {
|
|
176
|
+
summaryLines.pop();
|
|
177
|
+
}
|
|
178
|
+
summaryLines = trimTrailingBlankLines(summaryLines);
|
|
179
|
+
|
|
180
|
+
const linkLines = [];
|
|
181
|
+
for (let i = firstLinkIndex; i < lines.length; i += 1) {
|
|
182
|
+
const line = lines[i];
|
|
183
|
+
if (isLinksHeading(line)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (isDeliverableLinkLine(line)) {
|
|
187
|
+
const url = extractDeliverableURL(stripBulletPrefix(line));
|
|
188
|
+
if (url) {
|
|
189
|
+
linkLines.push(url);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const summary = summaryLines.join("\n").trim();
|
|
195
|
+
const links = linkLines.length > 0 ? linkLines[0] : "";
|
|
196
|
+
if (!summary || !links) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return { summary, links };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cloneBody(body, content, suffix) {
|
|
204
|
+
const next = {};
|
|
205
|
+
Object.keys(body).forEach((key) => {
|
|
206
|
+
next[key] = body[key];
|
|
207
|
+
});
|
|
208
|
+
next.content = content;
|
|
209
|
+
if (isString(body.msg_id) && body.msg_id.trim()) {
|
|
210
|
+
next.msg_id = body.msg_id + suffix;
|
|
211
|
+
}
|
|
212
|
+
return next;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function shouldSplitPalzPayload(body) {
|
|
216
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
if (!isString(body.content)) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
body.stream_id ||
|
|
224
|
+
body.seq !== undefined ||
|
|
225
|
+
body.delta !== undefined ||
|
|
226
|
+
body.is_final !== undefined
|
|
227
|
+
) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
if (body.palz_msg_type || body.tool_content) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return splitDeliverableMessage(body.content);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resolveFetchURL(input) {
|
|
237
|
+
if (isString(input)) {
|
|
238
|
+
return input;
|
|
239
|
+
}
|
|
240
|
+
if (input && isString(input.url)) {
|
|
241
|
+
return input.url;
|
|
242
|
+
}
|
|
243
|
+
return "";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolveFetchMethod(input, init) {
|
|
247
|
+
if (init && isString(init.method) && init.method.trim()) {
|
|
248
|
+
return init.method.trim().toUpperCase();
|
|
249
|
+
}
|
|
250
|
+
if (input && !isString(input) && isString(input.method) && input.method.trim()) {
|
|
251
|
+
return input.method.trim().toUpperCase();
|
|
252
|
+
}
|
|
253
|
+
return "GET";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function previewText(text, limit = 1000) {
|
|
257
|
+
const normalized = String(text || "");
|
|
258
|
+
if (normalized.length <= limit) {
|
|
259
|
+
return normalized;
|
|
260
|
+
}
|
|
261
|
+
return `${normalized.slice(0, limit)}...<truncated>`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function readResponseText(response) {
|
|
265
|
+
if (!response || typeof response.clone !== "function") {
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
return await response.clone().text();
|
|
270
|
+
} catch (_err) {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function installPalzFetchPatch(api) {
|
|
276
|
+
if (globalThis[FETCH_PATCH_KEY] && globalThis[FETCH_PATCH_KEY].installed) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const originalFetch = globalThis.fetch;
|
|
280
|
+
if (typeof originalFetch !== "function") {
|
|
281
|
+
api.logger.warn?.("[plugin-deliverables] global fetch is unavailable; split patch skipped");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const patchedFetch = async function(input, init) {
|
|
286
|
+
const url = resolveFetchURL(input);
|
|
287
|
+
const method = resolveFetchMethod(input, init);
|
|
288
|
+
if (method !== "POST" || !/\/bot\/send(?:\?|$)/.test(url)) {
|
|
289
|
+
return originalFetch(input, init);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const requestBody = init && init.body;
|
|
293
|
+
if (!isString(requestBody)) {
|
|
294
|
+
return originalFetch(input, init);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let parsed;
|
|
298
|
+
try {
|
|
299
|
+
parsed = JSON.parse(requestBody);
|
|
300
|
+
} catch (_err) {
|
|
301
|
+
return originalFetch(input, init);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const split = shouldSplitPalzPayload(parsed);
|
|
305
|
+
if (!split) {
|
|
306
|
+
return originalFetch(input, init);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const summaryBody = cloneBody(parsed, split.summary, "__summary");
|
|
310
|
+
const linksBody = cloneBody(parsed, split.links, "__links");
|
|
311
|
+
const summaryBodyStr = JSON.stringify(summaryBody);
|
|
312
|
+
const linksBodyStr = JSON.stringify(linksBody);
|
|
313
|
+
|
|
314
|
+
api.logger.info?.(
|
|
315
|
+
`[plugin-deliverables] split deliverable reply for palz target=${String(parsed.conversation_id || "")} summaryMsgId=${String(summaryBody.msg_id || "")} linksMsgId=${String(linksBody.msg_id || "")}`,
|
|
316
|
+
);
|
|
317
|
+
api.logger.info?.(
|
|
318
|
+
`[plugin-deliverables] palz summary request body_length=${summaryBodyStr.length}\n request_body=${summaryBodyStr}`,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const baseInit = Object.assign({}, init);
|
|
322
|
+
const summaryInit = Object.assign({}, baseInit, {
|
|
323
|
+
body: summaryBodyStr,
|
|
324
|
+
});
|
|
325
|
+
const linksInit = Object.assign({}, baseInit, {
|
|
326
|
+
body: linksBodyStr,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const firstResponse = await originalFetch(input, summaryInit);
|
|
330
|
+
const firstResponseText = await readResponseText(firstResponse);
|
|
331
|
+
api.logger.info?.(
|
|
332
|
+
`[plugin-deliverables] palz summary response status=${firstResponse ? firstResponse.status : "unknown"} ok=${Boolean(firstResponse && firstResponse.ok)}\n response_body=${previewText(firstResponseText)}`,
|
|
333
|
+
);
|
|
334
|
+
if (!firstResponse || !firstResponse.ok) {
|
|
335
|
+
return firstResponse;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
api.logger.info?.(
|
|
339
|
+
`[plugin-deliverables] palz links request body_length=${linksBodyStr.length}\n request_body=${linksBodyStr}`,
|
|
340
|
+
);
|
|
341
|
+
const secondResponse = await originalFetch(input, linksInit);
|
|
342
|
+
const secondResponseText = await readResponseText(secondResponse);
|
|
343
|
+
api.logger.info?.(
|
|
344
|
+
`[plugin-deliverables] palz links response status=${secondResponse ? secondResponse.status : "unknown"} ok=${Boolean(secondResponse && secondResponse.ok)}\n response_body=${previewText(secondResponseText)}`,
|
|
345
|
+
);
|
|
346
|
+
return secondResponse;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
globalThis.fetch = patchedFetch;
|
|
350
|
+
globalThis[FETCH_PATCH_KEY] = {
|
|
351
|
+
installed: true,
|
|
352
|
+
originalFetch,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
106
356
|
const plugin = {
|
|
107
357
|
id: "plugin-deliverables",
|
|
108
358
|
name: "Deliverables",
|
|
109
359
|
description: "Upload-first runtime guard for generated file deliverables.",
|
|
110
360
|
register(api) {
|
|
361
|
+
installPalzFetchPatch(api);
|
|
362
|
+
|
|
111
363
|
api.on("before_prompt_build", async () => ({
|
|
112
364
|
prependSystemContext: RUNTIME_DELIVERABLES_GUIDANCE,
|
|
113
365
|
}));
|
package/openclaw-plugin.json
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "plugin-deliverables",
|
|
3
3
|
"name": "Deliverables",
|
|
4
|
-
"description": "Deliverables runtime guard for upload-first file delivery.",
|
|
5
|
-
"version": "1.0.
|
|
4
|
+
"description": "Deliverables runtime guard for upload-first file delivery with Palz split-send diagnostics.",
|
|
5
|
+
"version": "1.0.16",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED