@dai_ming/plugin-deliverables 1.0.14 → 1.0.15

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,217 +1,149 @@
1
- #!/usr/bin/env node
2
1
  "use strict";
3
2
 
4
- var FETCH_PATCH_KEY = "__plugin_deliverables_palz_fetch_patch__";
5
-
6
- function isString(value) {
7
- return typeof value === "string";
8
- }
9
-
10
- function trimTrailingBlankLines(lines) {
11
- var out = lines.slice();
12
- while (out.length > 0 && /^\s*$/.test(out[out.length - 1])) {
13
- out.pop();
14
- }
15
- return out;
3
+ const RUNTIME_DELIVERABLES_GUIDANCE = [
4
+ "## Deliverables Runtime Guard (HARD CONSTRAINT)",
5
+ "",
6
+ "- These rules apply to the main agent and all subagents.",
7
+ "- 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`.",
8
+ "- 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.",
9
+ "- Do not emit `MEDIA:` file references for user-facing deliverables. Deliverable links must come from the deliverables upload tool instead.",
10
+ "- After a successful upload, reply with a short content-aware intro and then preserve the Markdown links returned by the deliverables tool.",
11
+ ].join("\n");
12
+
13
+ const ATTACHMENT_PARAM_KEYS = new Set([
14
+ "attachment",
15
+ "attachments",
16
+ "buffer",
17
+ "file",
18
+ "filepath",
19
+ "files",
20
+ "filename",
21
+ "media",
22
+ "mediaurl",
23
+ "mediaurls",
24
+ "path",
25
+ ]);
26
+
27
+ function normalizeToolName(toolName) {
28
+ return typeof toolName === "string" ? toolName.trim().toLowerCase() : "";
16
29
  }
17
30
 
18
- function stripBulletPrefix(line) {
19
- return String(line || "").replace(/^\s*[-*+]\s+/, "");
31
+ function isDeliverablesUploadTool(toolName) {
32
+ const normalized = normalizeToolName(toolName);
33
+ return normalized === "upload_deliverable" || normalized.endsWith("__upload_deliverable");
20
34
  }
21
35
 
22
- function isLinksHeading(line) {
23
- return /^\s*#{1,6}\s*访问链接\s*$/u.test(String(line || ""));
36
+ function isOutboundMessageTool(toolName) {
37
+ const normalized = normalizeToolName(toolName);
38
+ if (!normalized) {
39
+ return false;
40
+ }
41
+ return (
42
+ normalized === "message" ||
43
+ normalized.endsWith("__message") ||
44
+ normalized.includes("sendattachment") ||
45
+ normalized.includes("send_attachment")
46
+ );
24
47
  }
25
48
 
26
- function extractDeliverableURL(line) {
27
- var text = String(line || "").trim();
28
- if (!text) {
29
- return "";
30
- }
31
- var markdownLink = text.match(/\((https?:\/\/[^)\s]+)\)\s*$/iu);
32
- if (markdownLink && markdownLink[1]) {
33
- return markdownLink[1].trim();
49
+ function hasNonEmptyValue(value) {
50
+ if (typeof value === "string") {
51
+ return value.trim().length > 0;
34
52
  }
35
- var rawLink = text.match(/https?:\/\/\S+/iu);
36
- if (rawLink && rawLink[0]) {
37
- return rawLink[0].trim();
53
+ if (Array.isArray(value)) {
54
+ return value.length > 0;
38
55
  }
39
- return "";
40
- }
41
-
42
- function isDeliverableLinkLine(line) {
43
- var text = String(line || "");
44
- if (/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]\s*\[[^\]]+\]\([^)]+\)\s*$/u.test(text)) {
45
- return true;
56
+ if (value && typeof value === "object") {
57
+ return Object.keys(value).length > 0;
46
58
  }
47
- return !!extractDeliverableURL(text);
59
+ return Boolean(value);
48
60
  }
49
61
 
50
- function splitDeliverableMessage(text) {
51
- var normalized = String(text || "").replace(/\r\n/g, "\n").trim();
52
- if (!normalized) {
53
- return null;
54
- }
55
-
56
- var lines = normalized.split("\n");
57
- var firstLinkIndex = -1;
58
- var i;
59
- for (i = 0; i < lines.length; i += 1) {
60
- if (isDeliverableLinkLine(lines[i])) {
61
- firstLinkIndex = i;
62
- break;
63
- }
64
- }
65
- if (firstLinkIndex < 0) {
66
- return null;
67
- }
68
-
69
- var summaryLines = trimTrailingBlankLines(lines.slice(0, firstLinkIndex));
70
- if (summaryLines.length > 0 && isLinksHeading(summaryLines[summaryLines.length - 1])) {
71
- summaryLines.pop();
62
+ function containsMediaToken(value) {
63
+ if (typeof value === "string") {
64
+ return /(^|\n)\s*MEDIA:\s*\S+/i.test(value);
72
65
  }
73
- summaryLines = trimTrailingBlankLines(summaryLines);
74
-
75
- var linkLines = [];
76
- for (i = firstLinkIndex; i < lines.length; i += 1) {
77
- var line = lines[i];
78
- if (isLinksHeading(line)) {
79
- continue;
80
- }
81
- if (isDeliverableLinkLine(line)) {
82
- var url = extractDeliverableURL(stripBulletPrefix(line));
83
- if (url) {
84
- linkLines.push(url);
85
- }
86
- }
87
- }
88
-
89
- var summary = summaryLines.join("\n").trim();
90
- var links = linkLines.length > 0 ? linkLines[0] : "";
91
- if (!summary || !links) {
92
- return null;
66
+ if (Array.isArray(value)) {
67
+ return value.some(containsMediaToken);
93
68
  }
94
-
95
- return {
96
- summary: summary,
97
- links: links,
98
- };
99
- }
100
-
101
- function cloneBody(body, content, suffix) {
102
- var next = {};
103
- Object.keys(body).forEach(function(key) {
104
- next[key] = body[key];
105
- });
106
- next.content = content;
107
- if (isString(body.msg_id) && body.msg_id.trim()) {
108
- next.msg_id = body.msg_id + suffix;
69
+ if (!value || typeof value !== "object") {
70
+ return false;
109
71
  }
110
- return next;
72
+ return Object.values(value).some(containsMediaToken);
111
73
  }
112
74
 
113
- function shouldSplitPalzPayload(body) {
114
- if (!body || typeof body !== "object" || Array.isArray(body)) {
115
- return null;
116
- }
117
- if (!isString(body.content)) {
118
- return null;
75
+ function isDirectAttachmentBypass(params) {
76
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
77
+ return false;
119
78
  }
120
- if (body.stream_id || body.seq !== undefined || body.delta !== undefined || body.is_final !== undefined) {
121
- return null;
122
- }
123
- if (body.palz_msg_type || body.tool_content) {
124
- return null;
79
+ for (const [key, value] of Object.entries(params)) {
80
+ if (ATTACHMENT_PARAM_KEYS.has(String(key).trim().toLowerCase()) && hasNonEmptyValue(value)) {
81
+ return true;
82
+ }
125
83
  }
126
- return splitDeliverableMessage(body.content);
84
+ return containsMediaToken(params);
127
85
  }
128
86
 
129
- function resolveFetchURL(input) {
130
- if (isString(input)) {
131
- return input;
132
- }
133
- if (input && isString(input.url)) {
134
- return input.url;
87
+ function extractMediaUrls(metadata) {
88
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
89
+ return [];
135
90
  }
136
- return "";
137
- }
138
-
139
- function resolveFetchMethod(input, init) {
140
- if (init && isString(init.method) && init.method.trim()) {
141
- return init.method.trim().toUpperCase();
91
+ const urls = [];
92
+ const mediaUrls = metadata.mediaUrls;
93
+ if (Array.isArray(mediaUrls)) {
94
+ for (const entry of mediaUrls) {
95
+ if (typeof entry === "string" && entry.trim()) {
96
+ urls.push(entry.trim());
97
+ }
98
+ }
142
99
  }
143
- if (input && !isString(input) && isString(input.method) && input.method.trim()) {
144
- return input.method.trim().toUpperCase();
100
+ if (typeof metadata.mediaUrl === "string" && metadata.mediaUrl.trim()) {
101
+ urls.push(metadata.mediaUrl.trim());
145
102
  }
146
- return "GET";
103
+ return urls;
147
104
  }
148
105
 
149
- function installPalzFetchPatch(api) {
150
- if (globalThis[FETCH_PATCH_KEY] && globalThis[FETCH_PATCH_KEY].installed) {
151
- return;
152
- }
153
- var originalFetch = globalThis.fetch;
154
- if (typeof originalFetch !== "function") {
155
- api.logger.warn && api.logger.warn("[plugin-deliverables] global fetch is unavailable; split patch skipped");
156
- return;
157
- }
158
-
159
- var patchedFetch = async function(input, init) {
160
- var url = resolveFetchURL(input);
161
- var method = resolveFetchMethod(input, init);
162
- if (method !== "POST" || !/\/bot\/send(?:\?|$)/.test(url)) {
163
- return originalFetch(input, init);
164
- }
165
-
166
- var requestBody = init && init.body;
167
- if (!isString(requestBody)) {
168
- return originalFetch(input, init);
169
- }
170
-
171
- var parsed;
172
- try {
173
- parsed = JSON.parse(requestBody);
174
- } catch (err) {
175
- return originalFetch(input, init);
176
- }
177
-
178
- var split = shouldSplitPalzPayload(parsed);
179
- if (!split) {
180
- return originalFetch(input, init);
181
- }
182
-
183
- api.logger.info &&
184
- api.logger.info(
185
- "[plugin-deliverables] split deliverable reply for palz target=" +
186
- String(parsed.conversation_id || ""),
106
+ const plugin = {
107
+ id: "plugin-deliverables",
108
+ name: "Deliverables",
109
+ description: "Upload-first runtime guard for generated file deliverables.",
110
+ register(api) {
111
+ api.on("before_prompt_build", async () => ({
112
+ prependSystemContext: RUNTIME_DELIVERABLES_GUIDANCE,
113
+ }));
114
+
115
+ api.on("before_tool_call", async (event) => {
116
+ if (isDeliverablesUploadTool(event.toolName)) {
117
+ return;
118
+ }
119
+ if (!isOutboundMessageTool(event.toolName)) {
120
+ return;
121
+ }
122
+ if (!isDirectAttachmentBypass(event.params)) {
123
+ return;
124
+ }
125
+ api.logger.warn?.(
126
+ `plugin-deliverables: blocked direct file/message bypass via ${event.toolName}`,
187
127
  );
188
-
189
- var baseInit = Object.assign({}, init);
190
- var summaryInit = Object.assign({}, baseInit, {
191
- body: JSON.stringify(cloneBody(parsed, split.summary, "__summary")),
192
- });
193
- var linksInit = Object.assign({}, baseInit, {
194
- body: JSON.stringify(cloneBody(parsed, split.links, "__links")),
128
+ return {
129
+ block: true,
130
+ blockReason:
131
+ "Direct file/message delivery is disabled for deliverables. Write the artifact under output/ and call deliverables__upload_deliverable instead.",
132
+ };
195
133
  });
196
134
 
197
- var firstResponse = await originalFetch(input, summaryInit);
198
- if (!firstResponse || !firstResponse.ok) {
199
- return firstResponse;
200
- }
201
- return originalFetch(input, linksInit);
202
- };
203
-
204
- globalThis.fetch = patchedFetch;
205
- globalThis[FETCH_PATCH_KEY] = {
206
- installed: true,
207
- originalFetch: originalFetch,
208
- };
209
- }
210
-
211
- function register(api) {
212
- installPalzFetchPatch(api);
213
- }
135
+ api.on("message_sending", async (event, ctx) => {
136
+ const mediaUrls = extractMediaUrls(event.metadata);
137
+ if (mediaUrls.length === 0 && !containsMediaToken(event.content)) {
138
+ return;
139
+ }
140
+ api.logger.warn?.(
141
+ `plugin-deliverables: cancelled outbound media bypass on ${ctx.channelId || "unknown"} (${mediaUrls.length} media item(s))`,
142
+ );
143
+ return { cancel: true };
144
+ });
145
+ },
146
+ };
214
147
 
215
- module.exports = register;
216
- module.exports.default = register;
217
- module.exports.id = "plugin-deliverables";
148
+ module.exports = plugin;
149
+ module.exports.default = plugin;