@dai_ming/plugin-deliverables 1.0.16 → 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/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
  }
@@ -184,15 +434,15 @@ function splitDeliverableMessage(text) {
184
434
  continue;
185
435
  }
186
436
  if (isDeliverableLinkLine(line)) {
187
- const url = extractDeliverableURL(stripBulletPrefix(line));
188
- if (url) {
189
- linkLines.push(url);
437
+ const normalizedLine = stripBulletPrefix(line).trim();
438
+ if (normalizedLine) {
439
+ linkLines.push(normalizedLine);
190
440
  }
191
441
  }
192
442
  }
193
443
 
194
444
  const summary = summaryLines.join("\n").trim();
195
- const links = linkLines.length > 0 ? linkLines[0] : "";
445
+ const links = linkLines.join("\n").trim();
196
446
  if (!summary || !links) {
197
447
  return null;
198
448
  }
@@ -230,7 +480,19 @@ function shouldSplitPalzPayload(body) {
230
480
  if (body.palz_msg_type || body.tool_content) {
231
481
  return null;
232
482
  }
233
- return splitDeliverableMessage(body.content);
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;
234
496
  }
235
497
 
236
498
  function resolveFetchURL(input) {
@@ -366,6 +628,7 @@ const plugin = {
366
628
 
367
629
  api.on("before_tool_call", async (event) => {
368
630
  if (isDeliverablesUploadTool(event.toolName)) {
631
+ cacheUploadSummary(event.params);
369
632
  return;
370
633
  }
371
634
  if (!isOutboundMessageTool(event.toolName)) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.0.16",
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": {
@@ -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.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.16",
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",