@dai_ming/plugin-deliverables 1.0.14 → 1.0.16

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,14 +1,116 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
 
4
- var FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
3
+ const FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
4
+
5
+ const RUNTIME_DELIVERABLES_GUIDANCE = [
6
+ "## Deliverables Runtime Guard (HARD CONSTRAINT)",
7
+ "",
8
+ "- These rules apply to the main agent and all subagents.",
9
+ "- 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
+ "- 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
+ "- 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.",
13
+ ].join("\n");
5
14
 
6
15
  function isString(value) {
7
16
  return typeof value === "string";
8
17
  }
9
18
 
19
+ const ATTACHMENT_PARAM_KEYS = new Set([
20
+ "attachment",
21
+ "attachments",
22
+ "buffer",
23
+ "file",
24
+ "filepath",
25
+ "files",
26
+ "filename",
27
+ "media",
28
+ "mediaurl",
29
+ "mediaurls",
30
+ "path",
31
+ ]);
32
+
33
+ function normalizeToolName(toolName) {
34
+ return typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
35
+ }
36
+
37
+ function isDeliverablesUploadTool(toolName) {
38
+ const normalized = normalizeToolName(toolName);
39
+ return normalized === "upload_deliverable" || normalized.endsWith("__upload_deliverable");
40
+ }
41
+
42
+ function isOutboundMessageTool(toolName) {
43
+ const normalized = normalizeToolName(toolName);
44
+ if (!normalized) {
45
+ return false;
46
+ }
47
+ return (
48
+ normalized === "message" ||
49
+ normalized.endsWith("__message") ||
50
+ normalized.includes("sendattachment") ||
51
+ normalized.includes("send_attachment")
52
+ );
53
+ }
54
+
55
+ function hasNonEmptyValue(value) {
56
+ if (typeof value === "string") {
57
+ return value.trim().length > 0;
58
+ }
59
+ if (Array.isArray(value)) {
60
+ return value.length > 0;
61
+ }
62
+ if (value && typeof value === "object") {
63
+ return Object.keys(value).length > 0;
64
+ }
65
+ return Boolean(value);
66
+ }
67
+
68
+ function containsMediaToken(value) {
69
+ if (typeof value === "string") {
70
+ return /(^|\n)\s*MEDIA:\s*\S+/i.test(value);
71
+ }
72
+ if (Array.isArray(value)) {
73
+ return value.some(containsMediaToken);
74
+ }
75
+ if (!value || typeof value !== "object") {
76
+ return false;
77
+ }
78
+ return Object.values(value).some(containsMediaToken);
79
+ }
80
+
81
+ function isDirectAttachmentBypass(params) {
82
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
83
+ return false;
84
+ }
85
+ for (const [key, value] of Object.entries(params)) {
86
+ if (ATTACHMENT_PARAM_KEYS.has(String(key).trim().toLowerCase()) && hasNonEmptyValue(value)) {
87
+ return true;
88
+ }
89
+ }
90
+ return containsMediaToken(params);
91
+ }
92
+
93
+ function extractMediaUrls(metadata) {
94
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
95
+ return [];
96
+ }
97
+ const urls = [];
98
+ const mediaUrls = metadata.mediaUrls;
99
+ if (Array.isArray(mediaUrls)) {
100
+ for (const entry of mediaUrls) {
101
+ if (typeof entry === "string" && entry.trim()) {
102
+ urls.push(entry.trim());
103
+ }
104
+ }
105
+ }
106
+ if (typeof metadata.mediaUrl === "string" && metadata.mediaUrl.trim()) {
107
+ urls.push(metadata.mediaUrl.trim());
108
+ }
109
+ return urls;
110
+ }
111
+
10
112
  function trimTrailingBlankLines(lines) {
11
- var out = lines.slice();
113
+ const out = lines.slice();
12
114
  while (out.length > 0 && /^\s*$/.test(out[out.length - 1])) {
13
115
  out.pop();
14
116
  }
@@ -24,15 +126,15 @@ function isLinksHeading(line) {
24
126
  }
25
127
 
26
128
  function extractDeliverableURL(line) {
27
- var text = String(line || "").trim();
129
+ const text = String(line || "").trim();
28
130
  if (!text) {
29
131
  return "";
30
132
  }
31
- var markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
133
+ const markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
32
134
  if (markdownLink && markdownLink[1]) {
33
135
  return markdownLink[1].trim();
34
136
  }
35
- var rawLink = text.match(/https?:\/\/\S+/iu);
137
+ const rawLink = text.match(/https?:\/\/\S+/iu);
36
138
  if (rawLink && rawLink[0]) {
37
139
  return rawLink[0].trim();
38
140
  }
@@ -40,23 +142,26 @@ function extractDeliverableURL(line) {
40
142
  }
41
143
 
42
144
  function isDeliverableLinkLine(line) {
43
- var text = String(line || "");
44
- if (/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(text)) {
145
+ const text = String(line || "");
146
+ if (
147
+ /^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(
148
+ text,
149
+ )
150
+ ) {
45
151
  return true;
46
152
  }
47
153
  return !!extractDeliverableURL(text);
48
154
  }
49
155
 
50
156
  function splitDeliverableMessage(text) {
51
- var normalized = String(text || "").replace(/\r\n/g, "\n").trim();
157
+ const normalized = String(text || "").replace(/\r\n/g, "\n").trim();
52
158
  if (!normalized) {
53
159
  return null;
54
160
  }
55
161
 
56
- var lines = normalized.split("\n");
57
- var firstLinkIndex = -1;
58
- var i;
59
- for (i = 0; i < lines.length; i += 1) {
162
+ const lines = normalized.split("\n");
163
+ let firstLinkIndex = -1;
164
+ for (let i = 0; i < lines.length; i += 1) {
60
165
  if (isDeliverableLinkLine(lines[i])) {
61
166
  firstLinkIndex = i;
62
167
  break;
@@ -66,41 +171,38 @@ function splitDeliverableMessage(text) {
66
171
  return null;
67
172
  }
68
173
 
69
- var summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
174
+ let summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
70
175
  if (summaryLines.length > 0 && isLinksHeading(summaryLines[summaryLines.length - 1])) {
71
176
  summaryLines.pop();
72
177
  }
73
178
  summaryLines = trimTrailingBlankLines(summaryLines);
74
179
 
75
- var linkLines = [];
76
- for (i = firstLinkIndex; i < lines.length; i += 1) {
77
- var line = lines[i];
180
+ const linkLines = [];
181
+ for (let i = firstLinkIndex; i < lines.length; i += 1) {
182
+ const line = lines[i];
78
183
  if (isLinksHeading(line)) {
79
184
  continue;
80
185
  }
81
186
  if (isDeliverableLinkLine(line)) {
82
- var url = extractDeliverableURL(stripBulletPrefix(line));
187
+ const url = extractDeliverableURL(stripBulletPrefix(line));
83
188
  if (url) {
84
189
  linkLines.push(url);
85
190
  }
86
191
  }
87
192
  }
88
193
 
89
- var summary = summaryLines.join("\n").trim();
90
- var links = linkLines.length > 0 ? linkLines[0] : "";
194
+ const summary = summaryLines.join("\n").trim();
195
+ const links = linkLines.length > 0 ? linkLines[0] : "";
91
196
  if (!summary || !links) {
92
197
  return null;
93
198
  }
94
199
 
95
- return {
96
- summary: summary,
97
- links: links,
98
- };
200
+ return { summary, links };
99
201
  }
100
202
 
101
203
  function cloneBody(body, content, suffix) {
102
- var next = {};
103
- Object.keys(body).forEach(function(key) {
204
+ const next = {};
205
+ Object.keys(body).forEach((key) => {
104
206
  next[key] = body[key];
105
207
  });
106
208
  next.content = content;
@@ -117,7 +219,12 @@ function shouldSplitPalzPayload(body) {
117
219
  if (!isString(body.content)) {
118
220
  return null;
119
221
  }
120
- if (body.stream_id || body.seq !== undefined || body.delta !== undefined || body.is_final !== undefined) {
222
+ if (
223
+ body.stream_id ||
224
+ body.seq !== undefined ||
225
+ body.delta !== undefined ||
226
+ body.is_final !== undefined
227
+ ) {
121
228
  return null;
122
229
  }
123
230
  if (body.palz_msg_type || body.tool_content) {
@@ -146,72 +253,149 @@ function resolveFetchMethod(input, init) {
146
253
  return "GET";
147
254
  }
148
255
 
256
+ function previewText(text, limit = 1000) {
257
+ const normalized = String(text || "");
258
+ if (normalized.length <= limit) {
259
+ return normalized;
260
+ }
261
+ return `${normalized.slice(0, limit)}...<truncated>`;
262
+ }
263
+
264
+ async function readResponseText(response) {
265
+ if (!response || typeof response.clone !== "function") {
266
+ return "";
267
+ }
268
+ try {
269
+ return await response.clone().text();
270
+ } catch (_err) {
271
+ return "";
272
+ }
273
+ }
274
+
149
275
  function installPalzFetchPatch(api) {
150
276
  if (globalThis[FETCH_PATCH_KEY] && globalThis[FETCH_PATCH_KEY].installed) {
151
277
  return;
152
278
  }
153
- var originalFetch = globalThis.fetch;
279
+ const originalFetch = globalThis.fetch;
154
280
  if (typeof originalFetch !== "function") {
155
- api.logger.warn && api.logger.warn("[plugin-deliverables] global fetch is unavailable; split patch skipped");
281
+ api.logger.warn?.("[plugin-deliverables] global fetch is unavailable; split patch skipped");
156
282
  return;
157
283
  }
158
284
 
159
- var patchedFetch = async function(input, init) {
160
- var url = resolveFetchURL(input);
161
- var method = resolveFetchMethod(input, init);
285
+ const patchedFetch = async function(input, init) {
286
+ const url = resolveFetchURL(input);
287
+ const method = resolveFetchMethod(input, init);
162
288
  if (method !== "POST" || !/\/bot\/send(?:\?|$)/.test(url)) {
163
289
  return originalFetch(input, init);
164
290
  }
165
291
 
166
- var requestBody = init && init.body;
292
+ const requestBody = init && init.body;
167
293
  if (!isString(requestBody)) {
168
294
  return originalFetch(input, init);
169
295
  }
170
296
 
171
- var parsed;
297
+ let parsed;
172
298
  try {
173
299
  parsed = JSON.parse(requestBody);
174
- } catch (err) {
300
+ } catch (_err) {
175
301
  return originalFetch(input, init);
176
302
  }
177
303
 
178
- var split = shouldSplitPalzPayload(parsed);
304
+ const split = shouldSplitPalzPayload(parsed);
179
305
  if (!split) {
180
306
  return originalFetch(input, init);
181
307
  }
182
308
 
183
- api.logger.info &&
184
- api.logger.info(
185
- "[plugin-deliverables] split deliverable reply for palz target=" +
186
- String(parsed.conversation_id || ""),
187
- );
309
+ const summaryBody = cloneBody(parsed, split.summary, "__summary");
310
+ const linksBody = cloneBody(parsed, split.links, "__links");
311
+ const summaryBodyStr = JSON.stringify(summaryBody);
312
+ const linksBodyStr = JSON.stringify(linksBody);
188
313
 
189
- var baseInit = Object.assign({}, init);
190
- var summaryInit = Object.assign({}, baseInit, {
191
- body: JSON.stringify(cloneBody(parsed, split.summary, "__summary")),
314
+ 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 || "")}`,
316
+ );
317
+ api.logger.info?.(
318
+ `[plugin-deliverables] palz summary request body_length=${summaryBodyStr.length}\n request_body=${summaryBodyStr}`,
319
+ );
320
+
321
+ const baseInit = Object.assign({}, init);
322
+ const summaryInit = Object.assign({}, baseInit, {
323
+ body: summaryBodyStr,
192
324
  });
193
- var linksInit = Object.assign({}, baseInit, {
194
- body: JSON.stringify(cloneBody(parsed, split.links, "__links")),
325
+ const linksInit = Object.assign({}, baseInit, {
326
+ body: linksBodyStr,
195
327
  });
196
328
 
197
- var firstResponse = await originalFetch(input, summaryInit);
329
+ const firstResponse = await originalFetch(input, summaryInit);
330
+ const firstResponseText = await readResponseText(firstResponse);
331
+ api.logger.info?.(
332
+ `[plugin-deliverables] palz summary response status=${firstResponse ? firstResponse.status : "unknown"} ok=${Boolean(firstResponse && firstResponse.ok)}\n response_body=${previewText(firstResponseText)}`,
333
+ );
198
334
  if (!firstResponse || !firstResponse.ok) {
199
335
  return firstResponse;
200
336
  }
201
- return originalFetch(input, linksInit);
337
+
338
+ api.logger.info?.(
339
+ `[plugin-deliverables] palz links request body_length=${linksBodyStr.length}\n request_body=${linksBodyStr}`,
340
+ );
341
+ const secondResponse = await originalFetch(input, linksInit);
342
+ const secondResponseText = await readResponseText(secondResponse);
343
+ api.logger.info?.(
344
+ `[plugin-deliverables] palz links response status=${secondResponse ? secondResponse.status : "unknown"} ok=${Boolean(secondResponse && secondResponse.ok)}\n response_body=${previewText(secondResponseText)}`,
345
+ );
346
+ return secondResponse;
202
347
  };
203
348
 
204
349
  globalThis.fetch = patchedFetch;
205
350
  globalThis[FETCH_PATCH_KEY] = {
206
351
  installed: true,
207
- originalFetch: originalFetch,
352
+ originalFetch,
208
353
  };
209
354
  }
210
355
 
211
- function register(api) {
212
- installPalzFetchPatch(api);
213
- }
356
+ const plugin = {
357
+ id: "plugin-deliverables",
358
+ name: "Deliverables",
359
+ description: "Upload-first runtime guard for generated file deliverables.",
360
+ register(api) {
361
+ installPalzFetchPatch(api);
362
+
363
+ api.on("before_prompt_build", async () => ({
364
+ prependSystemContext: RUNTIME_DELIVERABLES_GUIDANCE,
365
+ }));
366
+
367
+ api.on("before_tool_call", async (event) => {
368
+ if (isDeliverablesUploadTool(event.toolName)) {
369
+ return;
370
+ }
371
+ if (!isOutboundMessageTool(event.toolName)) {
372
+ return;
373
+ }
374
+ if (!isDirectAttachmentBypass(event.params)) {
375
+ return;
376
+ }
377
+ api.logger.warn?.(
378
+ `plugin-deliverables: blocked direct file/message bypass via ${event.toolName}`,
379
+ );
380
+ return {
381
+ block: true,
382
+ blockReason:
383
+ "Direct file/message delivery is disabled for deliverables. Write the artifact under output/ and call deliverables__upload_deliverable instead.",
384
+ };
385
+ });
386
+
387
+ api.on("message_sending", async (event, ctx) => {
388
+ const mediaUrls = extractMediaUrls(event.metadata);
389
+ if (mediaUrls.length === 0 && !containsMediaToken(event.content)) {
390
+ return;
391
+ }
392
+ api.logger.warn?.(
393
+ `plugin-deliverables: cancelled outbound media bypass on ${ctx.channelId || "unknown"} (${mediaUrls.length} media item(s))`,
394
+ );
395
+ return { cancel: true };
396
+ });
397
+ },
398
+ };
214
399
 
215
- module.exports = register;
216
- module.exports.default = register;
217
- module.exports.id = "plugin-deliverables";
400
+ module.exports = plugin;
401
+ module.exports.default = plugin;