@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 +269 -6
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
188
|
-
if (
|
|
189
|
-
linkLines.push(
|
|
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.
|
|
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
|
-
|
|
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)) {
|
package/openclaw-plugin.json
CHANGED
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "1.0.17",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED