@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 +289 -9
- 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
|
}
|
|
@@ -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
|
|
188
|
-
if (
|
|
189
|
-
linkLines.push(
|
|
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.
|
|
450
|
+
const links = linkLines.join("\n").trim();
|
|
196
451
|
if (!summary || !links) {
|
|
197
452
|
return null;
|
|
198
453
|
}
|
|
199
454
|
|
|
200
|
-
return {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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)) {
|
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.18",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED