@dai_ming/plugin-deliverables 1.0.15 → 1.0.17

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # plugin-deliverables 安装文档
2
2
 
3
- 本文档描述 `@dai_ming/plugin-deliverables@1.0.15` 的安装、升级与验证方式。
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.15 --pin
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.15"
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.15 --registry https://registry.npmjs.org
70
- tar xzf dai_ming-plugin-deliverables-1.0.15.tgz \
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.15" }
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.15 --pin
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.15"
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.15` 下载 tarball → 解压到 `/data/extensions-extra/plugin-deliverables/`
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.15 |
226
+ | 当前 | 1.0.16 |
225
227
 
226
228
  ---
227
229
 
package/index.js CHANGED
@@ -1,5 +1,10 @@
1
1
  "use strict";
2
2
 
3
+ const FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
4
+ const SUMMARY_CACHE_KEY = "__plugin_deliverables_summary_cache__";
5
+ const SUMMARY_CACHE_LIMIT = 200;
6
+ const SHORT_SUMMARY_THRESHOLD = 120;
7
+
3
8
  const RUNTIME_DELIVERABLES_GUIDANCE = [
4
9
  "## Deliverables Runtime Guard (HARD CONSTRAINT)",
5
10
  "",
@@ -7,9 +12,20 @@ const RUNTIME_DELIVERABLES_GUIDANCE = [
7
12
  "- When the user asks for a document, article, report, HTML page, Markdown file, PPT, image, archive, game, or any other file deliverable, you MUST create it under the current workspace `output/` directory and upload it with `deliverables__upload_deliverable`.",
8
13
  "- Do not use direct message attachments to deliver generated files. This includes the message tool attachment fields such as `media`, `path`, `filePath`, `buffer`, `attachment`, or similar file-carrying fields.",
9
14
  "- Do not emit `MEDIA:` file references for user-facing deliverables. Deliverable links must come from the deliverables upload tool instead.",
10
- "- After a successful upload, reply with a short content-aware intro and then preserve the Markdown links returned by the deliverables tool.",
15
+ "- After a successful upload, reply with a substantive content summary before the links. For documents/articles/reports, prefer 1 short intro plus 3-6 concise bullets covering key sections, highlights, or findings, then preserve the Markdown links returned by the deliverables tool.",
11
16
  ].join("\n");
12
17
 
18
+ function isString(value) {
19
+ return typeof value === "string";
20
+ }
21
+
22
+ function getSummaryCache() {
23
+ if (!globalThis[SUMMARY_CACHE_KEY] || !(globalThis[SUMMARY_CACHE_KEY] instanceof Map)) {
24
+ globalThis[SUMMARY_CACHE_KEY] = new Map();
25
+ }
26
+ return globalThis[SUMMARY_CACHE_KEY];
27
+ }
28
+
13
29
  const ATTACHMENT_PARAM_KEYS = new Set([
14
30
  "attachment",
15
31
  "attachments",
@@ -103,17 +119,516 @@ function extractMediaUrls(metadata) {
103
119
  return urls;
104
120
  }
105
121
 
122
+ function trimTrailingBlankLines(lines) {
123
+ const out = lines.slice();
124
+ while (out.length > 0 && /^\s*$/.test(out[out.length - 1])) {
125
+ out.pop();
126
+ }
127
+ return out;
128
+ }
129
+
130
+ function stripBulletPrefix(line) {
131
+ return String(line || "").replace(/^\s*[-*+]\s+/, "");
132
+ }
133
+
134
+ function stripNumberedPrefix(line) {
135
+ return String(line || "").replace(/^\s*\d+[.)]\s+/, "");
136
+ }
137
+
138
+ function cleanInlineMarkdown(text) {
139
+ return String(text || "")
140
+ .replace(/!\[[^\]]*\]\([^)]+\)/g, "")
141
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
142
+ .replace(/`([^`]*)`/g, "$1")
143
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
144
+ .replace(/\*([^*]+)\*/g, "$1")
145
+ .replace(/__([^_]+)__/g, "$1")
146
+ .replace(/_([^_]+)_/g, "$1")
147
+ .replace(/^\s*>\s?/gm, "")
148
+ .replace(/\s+/g, " ")
149
+ .trim();
150
+ }
151
+
152
+ function trimSummaryText(text, limit = 160) {
153
+ const normalized = cleanInlineMarkdown(text);
154
+ if (normalized.length <= limit) {
155
+ return normalized;
156
+ }
157
+ return normalized.slice(0, limit - 1).trimEnd() + "…";
158
+ }
159
+
160
+ function extractTitleFromContent(text, fallbackName) {
161
+ const normalized = String(text || "").replace(/\r\n/g, "\n");
162
+ const lines = normalized.split("\n");
163
+ for (const line of lines) {
164
+ const match = line.match(/^\s*#\s+(.+?)\s*$/u);
165
+ if (match && match[1]) {
166
+ return trimSummaryText(match[1], 80);
167
+ }
168
+ }
169
+ return trimSummaryText(String(fallbackName || "").replace(/\.[^.]+$/, ""), 80);
170
+ }
171
+
172
+ function isBulletLine(line) {
173
+ return /^\s*[-*+]\s+/.test(String(line || ""));
174
+ }
175
+
176
+ function isNumberedLine(line) {
177
+ return /^\s*\d+[.)]\s+/.test(String(line || ""));
178
+ }
179
+
180
+ function summarizeSectionBody(lines) {
181
+ const bulletItems = [];
182
+ const paragraphLines = [];
183
+
184
+ for (const rawLine of lines) {
185
+ const line = String(rawLine || "").trim();
186
+ if (!line) {
187
+ continue;
188
+ }
189
+ if (isBulletLine(line)) {
190
+ bulletItems.push(trimSummaryText(stripBulletPrefix(line), 80));
191
+ continue;
192
+ }
193
+ if (isNumberedLine(line)) {
194
+ bulletItems.push(trimSummaryText(stripNumberedPrefix(line), 80));
195
+ continue;
196
+ }
197
+ if (/^\s*#{1,6}\s+/u.test(line)) {
198
+ continue;
199
+ }
200
+ paragraphLines.push(cleanInlineMarkdown(line));
201
+ }
202
+
203
+ if (bulletItems.length > 0) {
204
+ return bulletItems.slice(0, 2).join(";");
205
+ }
206
+
207
+ if (paragraphLines.length > 0) {
208
+ return trimSummaryText(paragraphLines.join(" "), 120);
209
+ }
210
+
211
+ return "";
212
+ }
213
+
214
+ function buildSummaryFromTextContent(text, fallbackName) {
215
+ const normalized = String(text || "").replace(/\r\n/g, "\n").trim();
216
+ if (!normalized) {
217
+ return "";
218
+ }
219
+
220
+ const title = extractTitleFromContent(normalized, fallbackName);
221
+ const lines = normalized.split("\n");
222
+ const sections = [];
223
+ let current = null;
224
+
225
+ for (const rawLine of lines) {
226
+ const line = String(rawLine || "");
227
+ const headingMatch = line.match(/^\s*#{2,3}\s+(.+?)\s*$/u);
228
+ if (headingMatch && headingMatch[1]) {
229
+ if (current) {
230
+ sections.push(current);
231
+ }
232
+ current = {
233
+ heading: trimSummaryText(headingMatch[1], 40),
234
+ lines: [],
235
+ };
236
+ continue;
237
+ }
238
+ if (current) {
239
+ current.lines.push(line);
240
+ }
241
+ }
242
+ if (current) {
243
+ sections.push(current);
244
+ }
245
+
246
+ const bulletLines = [];
247
+ for (const section of sections) {
248
+ const snippet = summarizeSectionBody(section.lines);
249
+ if (!snippet) {
250
+ continue;
251
+ }
252
+ bulletLines.push(`- ${section.heading}:${snippet}`);
253
+ if (bulletLines.length >= 4) {
254
+ break;
255
+ }
256
+ }
257
+
258
+ if (bulletLines.length === 0) {
259
+ const fallbackSnippet = summarizeSectionBody(lines);
260
+ if (!fallbackSnippet) {
261
+ return title ? `已为你生成《${title}》。` : "";
262
+ }
263
+ return title
264
+ ? `已为你生成《${title}》。\n\n内容摘要:\n- ${fallbackSnippet}`
265
+ : `内容摘要:\n- ${fallbackSnippet}`;
266
+ }
267
+
268
+ const intro = title ? `已为你生成《${title}》。` : "已为你生成交付物。";
269
+ return `${intro}\n\n内容摘要:\n${bulletLines.join("\n")}`;
270
+ }
271
+
272
+ function selectPrimaryTextFile(files) {
273
+ if (!Array.isArray(files) || files.length === 0) {
274
+ return null;
275
+ }
276
+
277
+ const textFiles = files.filter((file) => isString(file && file.content_text) && file.content_text.trim());
278
+ if (textFiles.length === 0) {
279
+ return null;
280
+ }
281
+
282
+ const scored = textFiles.map((file) => {
283
+ const name = String(file.name || "");
284
+ const content = String(file.content_text || "");
285
+ let score = content.length;
286
+ if (/\/?index\.html?$/i.test(name)) {
287
+ score += 2000;
288
+ }
289
+ if (/\.(md|markdown)$/i.test(name)) {
290
+ score += 1500;
291
+ }
292
+ if (/\.(txt|html?)$/i.test(name)) {
293
+ score += 800;
294
+ }
295
+ return { file, score };
296
+ });
297
+
298
+ scored.sort((a, b) => b.score - a.score);
299
+ return scored[0].file;
300
+ }
301
+
302
+ function buildSummaryFromUploadParams(params) {
303
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
304
+ return "";
305
+ }
306
+
307
+ if (isString(params.content_text) && params.content_text.trim()) {
308
+ return buildSummaryFromTextContent(params.content_text, params.file_name);
309
+ }
310
+
311
+ const primaryFile = selectPrimaryTextFile(params.files);
312
+ if (primaryFile && isString(primaryFile.content_text) && primaryFile.content_text.trim()) {
313
+ return buildSummaryFromTextContent(primaryFile.content_text, primaryFile.name || params.file_name);
314
+ }
315
+
316
+ if (Array.isArray(params.files) && params.files.length > 0) {
317
+ const names = params.files
318
+ .map((file) => trimSummaryText(file && file.name ? String(file.name) : "", 40))
319
+ .filter(Boolean)
320
+ .slice(0, 4);
321
+ if (names.length > 0) {
322
+ return `已为你生成《${trimSummaryText(String(params.file_name || "交付物"), 60)}》。\n\n内容摘要:\n- 共包含 ${params.files.length} 个文件\n- 主要文件:${names.join("、")}`;
323
+ }
324
+ }
325
+
326
+ return "";
327
+ }
328
+
329
+ function cacheUploadSummary(params) {
330
+ const resourceId = isString(params && params.resource_id) ? params.resource_id.trim() : "";
331
+ if (!resourceId) {
332
+ return;
333
+ }
334
+ const summary = buildSummaryFromUploadParams(params);
335
+ if (!summary) {
336
+ return;
337
+ }
338
+ const cache = getSummaryCache();
339
+ cache.set(resourceId, {
340
+ summary,
341
+ createdAt: Date.now(),
342
+ });
343
+ while (cache.size > SUMMARY_CACHE_LIMIT) {
344
+ const firstKey = cache.keys().next();
345
+ if (firstKey.done) {
346
+ break;
347
+ }
348
+ cache.delete(firstKey.value);
349
+ }
350
+ }
351
+
352
+ function takeCachedSummary(resourceId) {
353
+ const key = isString(resourceId) ? resourceId.trim() : "";
354
+ if (!key) {
355
+ return "";
356
+ }
357
+ const cache = getSummaryCache();
358
+ const entry = cache.get(key);
359
+ cache.delete(key);
360
+ return entry && isString(entry.summary) ? entry.summary.trim() : "";
361
+ }
362
+
363
+ function isSummaryTooShort(summary) {
364
+ const text = cleanInlineMarkdown(summary);
365
+ if (!text) {
366
+ return true;
367
+ }
368
+ if (text.length < SHORT_SUMMARY_THRESHOLD) {
369
+ return true;
370
+ }
371
+ return !/\n\s*[-*+]\s+/u.test(String(summary || ""));
372
+ }
373
+
374
+ function isLinksHeading(line) {
375
+ return /^\s*#{1,6}\s*访问链接\s*$/u.test(String(line || ""));
376
+ }
377
+
378
+ function extractDeliverableURL(line) {
379
+ const text = String(line || "").trim();
380
+ if (!text) {
381
+ return "";
382
+ }
383
+ const markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
384
+ if (markdownLink && markdownLink[1]) {
385
+ return markdownLink[1].trim();
386
+ }
387
+ const rawLink = text.match(/https?:\/\/\S+/iu);
388
+ if (rawLink && rawLink[0]) {
389
+ return rawLink[0].trim();
390
+ }
391
+ return "";
392
+ }
393
+
394
+ function isDeliverableLinkLine(line) {
395
+ const text = String(line || "");
396
+ if (
397
+ /^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(
398
+ text,
399
+ )
400
+ ) {
401
+ return true;
402
+ }
403
+ return !!extractDeliverableURL(text);
404
+ }
405
+
406
+ function splitDeliverableMessage(text) {
407
+ const normalized = String(text || "").replace(/\r\n/g, "\n").trim();
408
+ if (!normalized) {
409
+ return null;
410
+ }
411
+
412
+ const lines = normalized.split("\n");
413
+ let firstLinkIndex = -1;
414
+ for (let i = 0; i < lines.length; i += 1) {
415
+ if (isDeliverableLinkLine(lines[i])) {
416
+ firstLinkIndex = i;
417
+ break;
418
+ }
419
+ }
420
+ if (firstLinkIndex < 0) {
421
+ return null;
422
+ }
423
+
424
+ let summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
425
+ if (summaryLines.length > 0 && isLinksHeading(summaryLines[summaryLines.length - 1])) {
426
+ summaryLines.pop();
427
+ }
428
+ summaryLines = trimTrailingBlankLines(summaryLines);
429
+
430
+ const linkLines = [];
431
+ for (let i = firstLinkIndex; i < lines.length; i += 1) {
432
+ const line = lines[i];
433
+ if (isLinksHeading(line)) {
434
+ continue;
435
+ }
436
+ if (isDeliverableLinkLine(line)) {
437
+ const normalizedLine = stripBulletPrefix(line).trim();
438
+ if (normalizedLine) {
439
+ linkLines.push(normalizedLine);
440
+ }
441
+ }
442
+ }
443
+
444
+ const summary = summaryLines.join("\n").trim();
445
+ const links = linkLines.join("\n").trim();
446
+ if (!summary || !links) {
447
+ return null;
448
+ }
449
+
450
+ return { summary, links };
451
+ }
452
+
453
+ function cloneBody(body, content, suffix) {
454
+ const next = {};
455
+ Object.keys(body).forEach((key) => {
456
+ next[key] = body[key];
457
+ });
458
+ next.content = content;
459
+ if (isString(body.msg_id) && body.msg_id.trim()) {
460
+ next.msg_id = body.msg_id + suffix;
461
+ }
462
+ return next;
463
+ }
464
+
465
+ function shouldSplitPalzPayload(body) {
466
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
467
+ return null;
468
+ }
469
+ if (!isString(body.content)) {
470
+ return null;
471
+ }
472
+ if (
473
+ body.stream_id ||
474
+ body.seq !== undefined ||
475
+ body.delta !== undefined ||
476
+ body.is_final !== undefined
477
+ ) {
478
+ return null;
479
+ }
480
+ if (body.palz_msg_type || body.tool_content) {
481
+ return null;
482
+ }
483
+ const split = splitDeliverableMessage(body.content);
484
+ if (!split) {
485
+ return null;
486
+ }
487
+
488
+ const cachedSummary = takeCachedSummary(body.resource_id);
489
+ if (cachedSummary && isSummaryTooShort(split.summary)) {
490
+ return {
491
+ summary: cachedSummary,
492
+ links: split.links,
493
+ };
494
+ }
495
+ return split;
496
+ }
497
+
498
+ function resolveFetchURL(input) {
499
+ if (isString(input)) {
500
+ return input;
501
+ }
502
+ if (input && isString(input.url)) {
503
+ return input.url;
504
+ }
505
+ return "";
506
+ }
507
+
508
+ function resolveFetchMethod(input, init) {
509
+ if (init && isString(init.method) && init.method.trim()) {
510
+ return init.method.trim().toUpperCase();
511
+ }
512
+ if (input && !isString(input) && isString(input.method) && input.method.trim()) {
513
+ return input.method.trim().toUpperCase();
514
+ }
515
+ return "GET";
516
+ }
517
+
518
+ function previewText(text, limit = 1000) {
519
+ const normalized = String(text || "");
520
+ if (normalized.length <= limit) {
521
+ return normalized;
522
+ }
523
+ return `${normalized.slice(0, limit)}...<truncated>`;
524
+ }
525
+
526
+ async function readResponseText(response) {
527
+ if (!response || typeof response.clone !== "function") {
528
+ return "";
529
+ }
530
+ try {
531
+ return await response.clone().text();
532
+ } catch (_err) {
533
+ return "";
534
+ }
535
+ }
536
+
537
+ function installPalzFetchPatch(api) {
538
+ if (globalThis[FETCH_PATCH_KEY] && globalThis[FETCH_PATCH_KEY].installed) {
539
+ return;
540
+ }
541
+ const originalFetch = globalThis.fetch;
542
+ if (typeof originalFetch !== "function") {
543
+ api.logger.warn?.("[plugin-deliverables] global fetch is unavailable; split patch skipped");
544
+ return;
545
+ }
546
+
547
+ const patchedFetch = async function(input, init) {
548
+ const url = resolveFetchURL(input);
549
+ const method = resolveFetchMethod(input, init);
550
+ if (method !== "POST" || !/\/bot\/send(?:\?|$)/.test(url)) {
551
+ return originalFetch(input, init);
552
+ }
553
+
554
+ const requestBody = init && init.body;
555
+ if (!isString(requestBody)) {
556
+ return originalFetch(input, init);
557
+ }
558
+
559
+ let parsed;
560
+ try {
561
+ parsed = JSON.parse(requestBody);
562
+ } catch (_err) {
563
+ return originalFetch(input, init);
564
+ }
565
+
566
+ const split = shouldSplitPalzPayload(parsed);
567
+ if (!split) {
568
+ return originalFetch(input, init);
569
+ }
570
+
571
+ const summaryBody = cloneBody(parsed, split.summary, "__summary");
572
+ const linksBody = cloneBody(parsed, split.links, "__links");
573
+ const summaryBodyStr = JSON.stringify(summaryBody);
574
+ const linksBodyStr = JSON.stringify(linksBody);
575
+
576
+ api.logger.info?.(
577
+ `[plugin-deliverables] split deliverable reply for palz target=${String(parsed.conversation_id || "")} summaryMsgId=${String(summaryBody.msg_id || "")} linksMsgId=${String(linksBody.msg_id || "")}`,
578
+ );
579
+ api.logger.info?.(
580
+ `[plugin-deliverables] palz summary request body_length=${summaryBodyStr.length}\n request_body=${summaryBodyStr}`,
581
+ );
582
+
583
+ const baseInit = Object.assign({}, init);
584
+ const summaryInit = Object.assign({}, baseInit, {
585
+ body: summaryBodyStr,
586
+ });
587
+ const linksInit = Object.assign({}, baseInit, {
588
+ body: linksBodyStr,
589
+ });
590
+
591
+ const firstResponse = await originalFetch(input, summaryInit);
592
+ const firstResponseText = await readResponseText(firstResponse);
593
+ api.logger.info?.(
594
+ `[plugin-deliverables] palz summary response status=${firstResponse ? firstResponse.status : "unknown"} ok=${Boolean(firstResponse && firstResponse.ok)}\n response_body=${previewText(firstResponseText)}`,
595
+ );
596
+ if (!firstResponse || !firstResponse.ok) {
597
+ return firstResponse;
598
+ }
599
+
600
+ api.logger.info?.(
601
+ `[plugin-deliverables] palz links request body_length=${linksBodyStr.length}\n request_body=${linksBodyStr}`,
602
+ );
603
+ const secondResponse = await originalFetch(input, linksInit);
604
+ const secondResponseText = await readResponseText(secondResponse);
605
+ api.logger.info?.(
606
+ `[plugin-deliverables] palz links response status=${secondResponse ? secondResponse.status : "unknown"} ok=${Boolean(secondResponse && secondResponse.ok)}\n response_body=${previewText(secondResponseText)}`,
607
+ );
608
+ return secondResponse;
609
+ };
610
+
611
+ globalThis.fetch = patchedFetch;
612
+ globalThis[FETCH_PATCH_KEY] = {
613
+ installed: true,
614
+ originalFetch,
615
+ };
616
+ }
617
+
106
618
  const plugin = {
107
619
  id: "plugin-deliverables",
108
620
  name: "Deliverables",
109
621
  description: "Upload-first runtime guard for generated file deliverables.",
110
622
  register(api) {
623
+ installPalzFetchPatch(api);
624
+
111
625
  api.on("before_prompt_build", async () => ({
112
626
  prependSystemContext: RUNTIME_DELIVERABLES_GUIDANCE,
113
627
  }));
114
628
 
115
629
  api.on("before_tool_call", async (event) => {
116
630
  if (isDeliverablesUploadTool(event.toolName)) {
631
+ cacheUploadSummary(event.params);
117
632
  return;
118
633
  }
119
634
  if (!isOutboundMessageTool(event.toolName)) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
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": {
@@ -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.15",
4
+ "description": "Deliverables runtime guard for upload-first file delivery with Palz split-send diagnostics.",
5
+ "version": "1.0.17",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dai_ming/plugin-deliverables",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
5
5
  "keywords": [
6
6
  "openclaw",