@dai_ming/plugin-deliverables 1.0.16 → 1.0.18

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/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
 
3
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;
4
7
 
5
8
  const RUNTIME_DELIVERABLES_GUIDANCE = [
6
9
  "## Deliverables Runtime Guard (HARD CONSTRAINT)",
@@ -9,13 +12,20 @@ const RUNTIME_DELIVERABLES_GUIDANCE = [
9
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`.",
10
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.",
11
14
  "- Do not emit `MEDIA:` file references for user-facing deliverables. Deliverable links must come from the deliverables upload tool instead.",
12
- "- 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.",
13
16
  ].join("\n");
14
17
 
15
18
  function isString(value) {
16
19
  return typeof value === "string";
17
20
  }
18
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
+
19
29
  const ATTACHMENT_PARAM_KEYS = new Set([
20
30
  "attachment",
21
31
  "attachments",
@@ -121,6 +131,246 @@ function stripBulletPrefix(line) {
121
131
  return String(line || "").replace(/^\s*[-*+]\s+/, "");
122
132
  }
123
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
+
124
374
  function isLinksHeading(line) {
125
375
  return /^\s*#{1,6}\s*访问链接\s*$/u.test(String(line || ""));
126
376
  }
@@ -178,26 +428,35 @@ function splitDeliverableMessage(text) {
178
428
  summaryLines = trimTrailingBlankLines(summaryLines);
179
429
 
180
430
  const linkLines = [];
431
+ const linkUrls = [];
181
432
  for (let i = firstLinkIndex; i < lines.length; i += 1) {
182
433
  const line = lines[i];
183
434
  if (isLinksHeading(line)) {
184
435
  continue;
185
436
  }
186
437
  if (isDeliverableLinkLine(line)) {
187
- const url = extractDeliverableURL(stripBulletPrefix(line));
188
- if (url) {
189
- linkLines.push(url);
438
+ const normalizedLine = stripBulletPrefix(line).trim();
439
+ if (normalizedLine) {
440
+ linkLines.push(normalizedLine);
441
+ const url = extractDeliverableURL(normalizedLine);
442
+ if (url) {
443
+ linkUrls.push(url);
444
+ }
190
445
  }
191
446
  }
192
447
  }
193
448
 
194
449
  const summary = summaryLines.join("\n").trim();
195
- const links = linkLines.length > 0 ? linkLines[0] : "";
450
+ const links = linkLines.join("\n").trim();
196
451
  if (!summary || !links) {
197
452
  return null;
198
453
  }
199
454
 
200
- return { summary, links };
455
+ return {
456
+ summary,
457
+ links,
458
+ primaryLinkUrl: linkUrls.length > 0 ? linkUrls[0] : "",
459
+ };
201
460
  }
202
461
 
203
462
  function cloneBody(body, content, suffix) {
@@ -212,6 +471,12 @@ function cloneBody(body, content, suffix) {
212
471
  return next;
213
472
  }
214
473
 
474
+ function cloneBodyAsFileLink(body, fileUrl, suffix) {
475
+ const next = cloneBody(body, [{ type: "file", file_url: { url: fileUrl } }], suffix);
476
+ delete next.msg_type;
477
+ return next;
478
+ }
479
+
215
480
  function shouldSplitPalzPayload(body) {
216
481
  if (!body || typeof body !== "object" || Array.isArray(body)) {
217
482
  return null;
@@ -230,7 +495,19 @@ function shouldSplitPalzPayload(body) {
230
495
  if (body.palz_msg_type || body.tool_content) {
231
496
  return null;
232
497
  }
233
- return splitDeliverableMessage(body.content);
498
+ const split = splitDeliverableMessage(body.content);
499
+ if (!split) {
500
+ return null;
501
+ }
502
+
503
+ const cachedSummary = takeCachedSummary(body.resource_id);
504
+ if (cachedSummary && isSummaryTooShort(split.summary)) {
505
+ return {
506
+ summary: cachedSummary,
507
+ links: split.links,
508
+ };
509
+ }
510
+ return split;
234
511
  }
235
512
 
236
513
  function resolveFetchURL(input) {
@@ -307,12 +584,14 @@ function installPalzFetchPatch(api) {
307
584
  }
308
585
 
309
586
  const summaryBody = cloneBody(parsed, split.summary, "__summary");
310
- const linksBody = cloneBody(parsed, split.links, "__links");
587
+ const linksBody = split.primaryLinkUrl
588
+ ? cloneBodyAsFileLink(parsed, split.primaryLinkUrl, "__links")
589
+ : cloneBody(parsed, split.links, "__links");
311
590
  const summaryBodyStr = JSON.stringify(summaryBody);
312
591
  const linksBodyStr = JSON.stringify(linksBody);
313
592
 
314
593
  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 || "")}`,
594
+ `[plugin-deliverables] split deliverable reply injected by plugin-deliverables target=${String(parsed.conversation_id || "")} summaryMsgId=${String(summaryBody.msg_id || "")} linksMsgId=${String(linksBody.msg_id || "")} linksMode=${split.primaryLinkUrl ? "file_url" : "text"}`,
316
595
  );
317
596
  api.logger.info?.(
318
597
  `[plugin-deliverables] palz summary request body_length=${summaryBodyStr.length}\n request_body=${summaryBodyStr}`,
@@ -366,6 +645,7 @@ const plugin = {
366
645
 
367
646
  api.on("before_tool_call", async (event) => {
368
647
  if (isDeliverablesUploadTool(event.toolName)) {
648
+ cacheUploadSummary(event.params);
369
649
  return;
370
650
  }
371
651
  if (!isOutboundMessageTool(event.toolName)) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
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": {
@@ -2,7 +2,7 @@
2
2
  "id": "plugin-deliverables",
3
3
  "name": "Deliverables",
4
4
  "description": "Deliverables runtime guard for upload-first file delivery with Palz split-send diagnostics.",
5
- "version": "1.0.16",
5
+ "version": "1.0.18",
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.16",
3
+ "version": "1.0.18",
4
4
  "description": "OpenClaw deliverables plugin — upload AI-generated files to OSS and return shareable preview/download links",
5
5
  "keywords": [
6
6
  "openclaw",