@dai_ming/plugin-deliverables 1.1.4 → 1.1.6
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/README.md +2 -2
- package/agents-rules/deliverables.md +19 -14
- package/deliverables.dev.config.json +1 -2
- package/deliverables.prod.config.json +1 -2
- package/deliverables.staging.config.json +1 -2
- package/index.js +205 -31
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/deliverables/SKILL.md +7 -7
package/README.md
CHANGED
|
@@ -47,9 +47,9 @@ deliverables__upload_deliverable
|
|
|
47
47
|
配置字段:
|
|
48
48
|
|
|
49
49
|
- `kbBaseUrl` — 知识库 API 内部地址(Pod 内通信)
|
|
50
|
-
- `kbPublicUrl` — 用户可见的预览链接地址
|
|
51
50
|
|
|
52
|
-
环境变量 `KNOWLEDGE_DB_URL`
|
|
51
|
+
环境变量 `KNOWLEDGE_DB_URL` 可覆盖配置文件值。`KNOWLEDGE_DB_API_KEY` 仅通过环境变量配置(默认 `kb-agent-key-2025`)。
|
|
52
|
+
IM 交付物链接信任校验使用固定前缀规则:`https://uniclaw-ai-kb`、`http://knowledge-db` 或 `http://localhost`。
|
|
53
53
|
|
|
54
54
|
## 发布
|
|
55
55
|
|
|
@@ -62,24 +62,29 @@ Tool name note:
|
|
|
62
62
|
7. If the user asks for PDF, upload a `.pdf` deliverable with `type=pdf`; do not substitute HTML or Markdown unless PDF generation truly failed and you clearly say so.
|
|
63
63
|
8. If the upload tool returns an error, retry with valid parameters or report the upload failure. Never invent OSS, preview, or download URLs.
|
|
64
64
|
9. If format is HTML, prefer `type=article` and file name ends with `.html`.
|
|
65
|
-
10.
|
|
66
|
-
11.
|
|
67
|
-
12. If
|
|
68
|
-
13.
|
|
69
|
-
14.
|
|
70
|
-
15.
|
|
71
|
-
16.
|
|
72
|
-
17.
|
|
73
|
-
18.
|
|
74
|
-
19.
|
|
75
|
-
20.
|
|
65
|
+
10. Local file names or relative paths such as `review.html` and `output/report.md` are not links. Do not use `type=link` for them; upload them with `file_path` or `content_text`.
|
|
66
|
+
11. Every single-file deliverable MUST use a file name with an explicit extension.
|
|
67
|
+
12. If format is markdown/text, use `type=article` and `.md`/`.txt`.
|
|
68
|
+
13. If the user did not specify a document/text format, default to Markdown and use a `.md` file name.
|
|
69
|
+
14. For multi-file deliverables (game/site), you MUST prefer `type=game` with `files[]`, keep files as a folder structure, and do NOT zip before upload unless the user explicitly asks for a zip package.
|
|
70
|
+
15. Static multi-file game/site deliverables SHOULD include a root `index.html` so the preview link can open the homepage directly.
|
|
71
|
+
16. If a generated project requires starting a separate backend service, custom port, database, or long-running process to work, do NOT pretend the deliverables preview can run it. Tell the user that deliverables preview only supports static output, and that runtime projects need deployment/ingress instead.
|
|
72
|
+
17. After successful upload, the final assistant message MUST start with 1-2 short sentences in your own words, briefly describing what you generated for the user.
|
|
73
|
+
18. For content deliverables such as articles, documents, reports, and PDFs, add 3-5 concise bullets after the intro summarizing the actual sections, highlights, or conclusions. Do not reply with only a one-sentence upload notice.
|
|
74
|
+
19. The intro and bullets must be based on the actual deliverable content/request, not a fixed sentence like `交付物已上传成功,可直接在线预览或下载。`
|
|
75
|
+
20. If tool result contains `reply_markdown`, append that field verbatim after your intro and content bullets on the following lines.
|
|
76
|
+
21. Otherwise final reply MUST include Markdown links (short label + full URL target):
|
|
76
77
|
`预览链接:[点击预览](<full_url>)`
|
|
77
78
|
`下载链接:[点击下载](<full_url>)`
|
|
78
|
-
|
|
79
|
+
22. For multi-file/game deliverables, if the tool gives directory-style output, the second line may be:
|
|
79
80
|
`文件列表:[查看目录](<full_url>)`
|
|
80
81
|
Keep that format instead of forcing a zip link.
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
23. Do NOT only say "已保存到工作空间".
|
|
83
|
+
24. Do NOT append workspace path or a raw URL block after the Markdown links.
|
|
84
|
+
25. 群聊场景(消息元数据包含 `is_group_chat == true` 或 `conversation_type == "group"`)调用上传工具时,MUST 传入 `group_id`(取自元数据的 `group_id` 或 `conversation_id`)和 `group_name`(取自元数据的 `group_name` 或 `group_subject`)。
|
|
85
|
+
26. 单聊/私聊场景,不传 `group_id` 或传 `"default"`,不传 `group_name`。
|
|
86
|
+
27. 无法判断会话类型时,若 `resource_id` 存在且不是 `"default"`,将 `resource_id` 作为 `group_id` 传入。
|
|
87
|
+
28. 调用上传工具时 MUST 传入 `user_id`:单聊取 `owner_id`,群聊取 `sender_id`。漏传 `user_id` 会导致 KB 上传失败(error 13002: 用户不存在),尤其在同时传了真实 `group_id` 的场景下。
|
|
83
88
|
|
|
84
89
|
### Exception
|
|
85
90
|
|
package/index.js
CHANGED
|
@@ -54,7 +54,7 @@ const AUTO_UPLOAD_MAX_FILES = 8;
|
|
|
54
54
|
const AUTO_UPLOAD_MAX_BYTES = 200 * 1024 * 1024;
|
|
55
55
|
const AUTO_UPLOAD_SCAN_MAX_ENTRIES = 4000;
|
|
56
56
|
const AUTO_UPLOAD_SCAN_DEPTH = 5;
|
|
57
|
-
const DELIVERABLE_KB_PATH_RE = /\/documents\/content
|
|
57
|
+
const DELIVERABLE_KB_PATH_RE = /\/documents\/(?:content|preview)(?:\/|$)/u;
|
|
58
58
|
const DELIVERABLE_LINK_PREFIX_RE =
|
|
59
59
|
/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]/u;
|
|
60
60
|
const DELIVERABLE_LINK_LABEL_RE =
|
|
@@ -63,6 +63,7 @@ const INTERNAL_ABSOLUTE_PATH_RE =
|
|
|
63
63
|
/(?:\/data\/workspace-[^\s`"'<>,。;:、))\]}]+|\/home\/node\/\.openclaw\/workspace(?:-[A-Za-z0-9_.-]+)?\/[^\s`"'<>,。;:、))\]}]+)/gu;
|
|
64
64
|
const INLINE_FILE_REF_RE = /`([^`\n]{1,512}\.[A-Za-z0-9]{1,16})`/gu;
|
|
65
65
|
const OUTPUT_RELATIVE_REF_RE = /(?:^|[\s(::])((?:\.\/)?output\/[^\s`"'<>,。;:、))\]}]+\.[A-Za-z0-9]{1,16})/gu;
|
|
66
|
+
const MARKDOWN_FILE_LINK_RE = /\[[^\]\n]{0,200}\]\(([^)\s]+?\.[A-Za-z0-9]{1,16})\)/gu;
|
|
66
67
|
const FILE_EXTENSION_RE = /\.[A-Za-z0-9]{1,16}$/u;
|
|
67
68
|
const TEXT_EXTENSIONS = new Set([
|
|
68
69
|
".css",
|
|
@@ -121,8 +122,11 @@ const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
|
121
122
|
"- Every single-file deliverable must use a file name with an explicit extension. If the user did not specify a document/text format, default to Markdown and use a `.md` file name.",
|
|
122
123
|
"- 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.",
|
|
123
124
|
"- Do not emit `MEDIA:` file references for user-facing deliverables. Deliverable links must come from the deliverables upload tool instead.",
|
|
125
|
+
"- Never use `type: \"link\"` for local files, relative paths, or generated files such as `review.html`; use `file_path`, `content_text`, or `files` so the file is uploaded.",
|
|
124
126
|
"- If the deliverables upload tool returns an error, retry with a valid `file_path` or explain the upload failure; never invent or rewrite OSS/download URLs.",
|
|
125
127
|
"- After a successful upload, reply with a substantive content summary before the links. For documents/articles/reports/PDFs, include 1 short intro plus 3-5 concise bullets covering the actual sections, highlights, or findings; never respond with only a generic one-sentence upload notice.",
|
|
128
|
+
"- CRITICAL: In group chats (metadata `is_group_chat == true` or `conversation_type == \"group\"`), you MUST pass `group_id` (from metadata `group_id` or `conversation_id`) and `group_name` (from metadata `group_name` or `group_subject`) to `deliverables__upload_deliverable`. For single/direct chats, omit `group_id` or pass `\"default\"`. Omitting `group_id` in a group chat causes the deliverable to land in the wrong knowledge base.",
|
|
129
|
+
"- CRITICAL: You MUST always pass `user_id` to `deliverables__upload_deliverable`. In single/direct chats, use `owner_id`; in group chats, use `sender_id`. Omitting `user_id` causes KB upload failures (error 13002: 用户不存在) when `group_id` is set to a real group.",
|
|
126
130
|
].join("\n");
|
|
127
131
|
|
|
128
132
|
const UPLOAD_DELIVERABLE_TOOL = {
|
|
@@ -158,7 +162,7 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
158
162
|
type: {
|
|
159
163
|
type: "string",
|
|
160
164
|
enum: ["article", "game", "zip", "image", "video", "ppt", "pdf", "link"],
|
|
161
|
-
description: "
|
|
165
|
+
description: "交付物类型。link 仅用于外部 http(s) URL;本地/生成文件必须使用 file_path、content_text 或 files。",
|
|
162
166
|
},
|
|
163
167
|
file_name: {
|
|
164
168
|
type: "string",
|
|
@@ -170,7 +174,7 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
170
174
|
},
|
|
171
175
|
content_text: {
|
|
172
176
|
type: "string",
|
|
173
|
-
description: "文本内容(Markdown、HTML 等)。
|
|
177
|
+
description: "文本内容(Markdown、HTML 等)。",
|
|
174
178
|
},
|
|
175
179
|
content_base64: {
|
|
176
180
|
type: "string",
|
|
@@ -241,6 +245,15 @@ function isURLLike(value) {
|
|
|
241
245
|
return /^[a-z][a-z0-9+.-]*:\/\//i.test(String(value || ""));
|
|
242
246
|
}
|
|
243
247
|
|
|
248
|
+
function isHTTPURLLike(value) {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = new URL(String(value || "").trim());
|
|
251
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && !!parsed.hostname;
|
|
252
|
+
} catch (_err) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
244
257
|
function fileExtension(filePath) {
|
|
245
258
|
return path.extname(stripTrailingPathPunctuation(filePath)).toLowerCase();
|
|
246
259
|
}
|
|
@@ -438,6 +451,9 @@ function extractFileReferencesFromText(text) {
|
|
|
438
451
|
for (const match of normalized.matchAll(OUTPUT_RELATIVE_REF_RE)) {
|
|
439
452
|
add(match[1], match[1], "relative");
|
|
440
453
|
}
|
|
454
|
+
for (const match of normalized.matchAll(MARKDOWN_FILE_LINK_RE)) {
|
|
455
|
+
add(match[0], match[1], "markdown-link");
|
|
456
|
+
}
|
|
441
457
|
return refs;
|
|
442
458
|
}
|
|
443
459
|
|
|
@@ -553,13 +569,87 @@ function payloadResourceId(body) {
|
|
|
553
569
|
}
|
|
554
570
|
|
|
555
571
|
function payloadGroupId(body) {
|
|
556
|
-
|
|
572
|
+
const explicit = extractStringField(body, ["group_id", "groupId"]);
|
|
573
|
+
if (explicit) {
|
|
574
|
+
return explicit;
|
|
575
|
+
}
|
|
576
|
+
const fallback = extractStringField(body, ["resource_id", "resourceId", "conversation_id", "conversationId"]);
|
|
577
|
+
return fallback && fallback !== "default" ? fallback : "";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function enrichDeliverableGroupContext(event) {
|
|
581
|
+
if (!event || !event.params || typeof event.params !== "object") {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const existingGroupId = trimString(event.params.group_id);
|
|
585
|
+
if (!existingGroupId) {
|
|
586
|
+
const inferredGroupId = extractContextString(event, [
|
|
587
|
+
"group_id", "groupId", "conversation_id", "conversationId",
|
|
588
|
+
]);
|
|
589
|
+
if (inferredGroupId && inferredGroupId !== "default") {
|
|
590
|
+
event.params.group_id = inferredGroupId;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const existingGroupName = trimString(event.params.group_name);
|
|
594
|
+
if (!existingGroupName) {
|
|
595
|
+
const inferredGroupName = extractContextString(event, [
|
|
596
|
+
"group_name", "groupName", "group_subject", "groupSubject",
|
|
597
|
+
]);
|
|
598
|
+
if (inferredGroupName) {
|
|
599
|
+
event.params.group_name = inferredGroupName;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const existingUserId = trimString(event.params.user_id);
|
|
603
|
+
if (!existingUserId || existingUserId === "default") {
|
|
604
|
+
const inferredUserId = extractContextString(event, [
|
|
605
|
+
"sender_id", "senderId", "owner_id", "ownerId", "user_id", "userId",
|
|
606
|
+
]);
|
|
607
|
+
if (inferredUserId && inferredUserId !== "default") {
|
|
608
|
+
event.params.user_id = inferredUserId;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
557
611
|
}
|
|
558
612
|
|
|
559
613
|
function payloadUserId(body) {
|
|
560
614
|
return extractStringField(body, ["user_id", "userId", "sender_id", "senderId", "owner_id", "ownerId"]);
|
|
561
615
|
}
|
|
562
616
|
|
|
617
|
+
function normalizeBareUserID(value) {
|
|
618
|
+
let raw = trimString(value);
|
|
619
|
+
if (!raw) {
|
|
620
|
+
return "";
|
|
621
|
+
}
|
|
622
|
+
if (raw.indexOf("user_") === 0 && raw.indexOf("_lobster_") > 0) {
|
|
623
|
+
raw = raw.slice("user_".length, raw.indexOf("_lobster_"));
|
|
624
|
+
} else if (raw.indexOf("user-") === 0 || raw.indexOf("user_") === 0) {
|
|
625
|
+
raw = raw.slice("user-".length);
|
|
626
|
+
}
|
|
627
|
+
return raw;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function userIDFromRelease(release) {
|
|
631
|
+
return normalizeBareUserID(release);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function userIDFromResourceID(resourceID) {
|
|
635
|
+
const raw = trimString(resourceID);
|
|
636
|
+
if (raw.indexOf("user_") !== 0 && raw.indexOf("user-") !== 0) {
|
|
637
|
+
return "";
|
|
638
|
+
}
|
|
639
|
+
return normalizeBareUserID(raw);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function deriveReleaseName() {
|
|
643
|
+
let release = process.env.botID || process.env.OPENCLAW_RELEASE || process.env.BOT_ID || "";
|
|
644
|
+
if (!release) {
|
|
645
|
+
const tok = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
646
|
+
if (tok.indexOf("oc-") === 0) {
|
|
647
|
+
release = tok.slice(3);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return release;
|
|
651
|
+
}
|
|
652
|
+
|
|
563
653
|
function isWriteTool(toolName) {
|
|
564
654
|
const normalized = normalizeToolName(toolName);
|
|
565
655
|
return normalized === "write" || normalized.endsWith("__write") || normalized === "write_file" || normalized.endsWith("__write_file");
|
|
@@ -625,11 +715,6 @@ function kbBaseURL() {
|
|
|
625
715
|
return (process.env.KNOWLEDGE_DB_URL || cfg.kbBaseUrl || DEFAULT_KB_BASE_URL).replace(/\/$/, "");
|
|
626
716
|
}
|
|
627
717
|
|
|
628
|
-
function kbPublicURL() {
|
|
629
|
-
const cfg = loadDeliverableConfig();
|
|
630
|
-
return (process.env.KNOWLEDGE_DB_PUBLIC_URL || cfg.kbPublicUrl || process.env.KNOWLEDGE_DB_URL || cfg.kbBaseUrl || DEFAULT_KB_BASE_URL).replace(/\/$/, "");
|
|
631
|
-
}
|
|
632
|
-
|
|
633
718
|
function kbApiKey() {
|
|
634
719
|
return process.env.KNOWLEDGE_DB_API_KEY || DEFAULT_KB_API_KEY;
|
|
635
720
|
}
|
|
@@ -642,25 +727,77 @@ function kbUploadPath(groupId, groupName) {
|
|
|
642
727
|
return "/" + gname + "/raw";
|
|
643
728
|
}
|
|
644
729
|
|
|
645
|
-
function
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
+ "&group_id=" + encodeURIComponent(trimString(groupId) || DEFAULT_GROUP_ID)
|
|
649
|
-
+ "&id=" + encodeURIComponent(String(documentId));
|
|
730
|
+
function kbUploadItems(response) {
|
|
731
|
+
const data = (response && response.data) || response || {};
|
|
732
|
+
return Array.isArray(data.items) ? data.items : [];
|
|
650
733
|
}
|
|
651
734
|
|
|
652
|
-
function
|
|
735
|
+
function kbFirstSuccessfulUploadItem(response) {
|
|
736
|
+
const items = kbUploadItems(response);
|
|
737
|
+
if (items.length === 0) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
return items.find((item) => item && !item.error && item.document) || items[0] || null;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function kbExtractDocument(response) {
|
|
744
|
+
const item = kbFirstSuccessfulUploadItem(response);
|
|
745
|
+
if (item && item.document && typeof item.document === "object" && !Array.isArray(item.document)) {
|
|
746
|
+
return item.document;
|
|
747
|
+
}
|
|
653
748
|
const data = (response && response.data) || response || {};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
749
|
+
if (data.document && typeof data.document === "object" && !Array.isArray(data.document)) {
|
|
750
|
+
return data.document;
|
|
751
|
+
}
|
|
752
|
+
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function kbExtractDocumentId(response) {
|
|
756
|
+
const document = kbExtractDocument(response);
|
|
757
|
+
if (document.id !== undefined) {
|
|
758
|
+
return document.id;
|
|
657
759
|
}
|
|
760
|
+
const data = (response && response.data) || response || {};
|
|
658
761
|
if (data.id !== undefined) {
|
|
659
762
|
return data.id;
|
|
660
763
|
}
|
|
661
764
|
return null;
|
|
662
765
|
}
|
|
663
766
|
|
|
767
|
+
function extractURLField(obj, keys) {
|
|
768
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
769
|
+
return "";
|
|
770
|
+
}
|
|
771
|
+
for (const key of keys) {
|
|
772
|
+
const value = obj[key];
|
|
773
|
+
if (isString(value) && value.trim()) {
|
|
774
|
+
return value.trim();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return "";
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function kbExtractReturnedLinks(response) {
|
|
781
|
+
const item = kbFirstSuccessfulUploadItem(response) || {};
|
|
782
|
+
const document = kbExtractDocument(response);
|
|
783
|
+
return {
|
|
784
|
+
downloadURL:
|
|
785
|
+
extractURLField(document, ["download_url", "downloadURL"]) ||
|
|
786
|
+
extractURLField(item, ["download_url", "downloadURL"]),
|
|
787
|
+
previewURL:
|
|
788
|
+
extractURLField(document, ["preview_url", "previewURL"]) ||
|
|
789
|
+
extractURLField(item, ["preview_url", "previewURL"]),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function requireKBReturnedLink(response) {
|
|
794
|
+
const links = kbExtractReturnedLinks(response);
|
|
795
|
+
if (!links.previewURL && !links.downloadURL) {
|
|
796
|
+
throw new Error("kb upload response missing preview_url/download_url");
|
|
797
|
+
}
|
|
798
|
+
return links;
|
|
799
|
+
}
|
|
800
|
+
|
|
664
801
|
function buildFileBuffersFromBody(body, isDirectory) {
|
|
665
802
|
if (isDirectory && Array.isArray(body.files)) {
|
|
666
803
|
return body.files.map((file) => ({
|
|
@@ -1029,7 +1166,11 @@ function buildNativeUploadRequestBody(args) {
|
|
|
1029
1166
|
if (fileCandidate) {
|
|
1030
1167
|
const stat = safeStat(fileCandidate.path);
|
|
1031
1168
|
const isDirectory = !!(stat && stat.isDirectory());
|
|
1032
|
-
|
|
1169
|
+
const requestedType = trimString(args.type);
|
|
1170
|
+
body.type =
|
|
1171
|
+
requestedType === "link"
|
|
1172
|
+
? deliverableTypeForPath(fileCandidate.path, isDirectory)
|
|
1173
|
+
: requestedType || deliverableTypeForPath(fileCandidate.path, isDirectory);
|
|
1033
1174
|
body.fileName = normalizeDeliverableFileName(args, fileCandidate.fileName);
|
|
1034
1175
|
if (isDirectory) {
|
|
1035
1176
|
const files = collectDirectoryFiles(fileCandidate.path);
|
|
@@ -1049,9 +1190,23 @@ function buildNativeUploadRequestBody(args) {
|
|
|
1049
1190
|
body.fileName = normalizeDeliverableFileName(args, "");
|
|
1050
1191
|
const files = normalizeNativeToolFiles(args && args.files);
|
|
1051
1192
|
if (files.length > 0) {
|
|
1193
|
+
if (body.type === "link") {
|
|
1194
|
+
body.type = "game";
|
|
1195
|
+
}
|
|
1052
1196
|
body.files = files;
|
|
1053
1197
|
return { body, isDirectory: true };
|
|
1054
1198
|
}
|
|
1199
|
+
if (body.type === "link") {
|
|
1200
|
+
const contentText = (args && (args.content_text || args.contentText)) || "";
|
|
1201
|
+
const contentBase64 = (args && (args.content_base64 || args.contentBase64)) || "";
|
|
1202
|
+
if (trimString(contentText) || trimString(contentBase64)) {
|
|
1203
|
+
body.type = deliverableTypeForPath(body.fileName, false);
|
|
1204
|
+
} else if (!isHTTPURLLike(body.fileName)) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
"type=link requires file_name to be an absolute http(s) URL; use file_path or content_text for local/generated files",
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1055
1210
|
const contentText = (args && (args.content_text || args.contentText)) || "";
|
|
1056
1211
|
const contentBase64 = (args && (args.content_base64 || args.contentBase64)) || "";
|
|
1057
1212
|
if (body.type === "pdf" && trimString(contentText) && !trimString(contentBase64)) {
|
|
@@ -1066,8 +1221,8 @@ function buildNativeUploadRequestBody(args) {
|
|
|
1066
1221
|
async function uploadDeliverable(args) {
|
|
1067
1222
|
const { body, isDirectory } = buildNativeUploadRequestBody(args || {});
|
|
1068
1223
|
const userId = payloadUserId(args) || "default";
|
|
1069
|
-
const groupId =
|
|
1070
|
-
const groupName =
|
|
1224
|
+
const groupId = payloadGroupId(args) || DEFAULT_GROUP_ID;
|
|
1225
|
+
const groupName = extractStringField(args || {}, ["group_name", "groupName", "group_subject", "groupSubject"]);
|
|
1071
1226
|
const uploadPath = kbUploadPath(groupId, groupName);
|
|
1072
1227
|
const fileBuffers = buildFileBuffersFromBody(body, isDirectory);
|
|
1073
1228
|
const fields = {
|
|
@@ -1077,8 +1232,9 @@ async function uploadDeliverable(args) {
|
|
|
1077
1232
|
};
|
|
1078
1233
|
const response = await kbMultipartUpload(fileBuffers, fields);
|
|
1079
1234
|
const documentId = kbExtractDocumentId(response);
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1235
|
+
const links = requireKBReturnedLink(response);
|
|
1236
|
+
const previewURL = links.previewURL || links.downloadURL;
|
|
1237
|
+
const downloadURL = links.downloadURL || previewURL;
|
|
1082
1238
|
const replyMarkdown = buildReplyMarkdown({
|
|
1083
1239
|
previewURL,
|
|
1084
1240
|
downloadURL,
|
|
@@ -1147,12 +1303,14 @@ async function uploadCandidate(candidate, body) {
|
|
|
1147
1303
|
path: uploadPath,
|
|
1148
1304
|
});
|
|
1149
1305
|
const documentId = kbExtractDocumentId(response);
|
|
1150
|
-
const
|
|
1306
|
+
const links = requireKBReturnedLink(response);
|
|
1307
|
+
const previewURL = links.previewURL || links.downloadURL;
|
|
1308
|
+
const downloadURL = links.downloadURL || previewURL;
|
|
1151
1309
|
const result = {
|
|
1152
1310
|
fileName,
|
|
1153
1311
|
document_id: documentId,
|
|
1154
1312
|
previewURL,
|
|
1155
|
-
downloadURL
|
|
1313
|
+
downloadURL,
|
|
1156
1314
|
};
|
|
1157
1315
|
cache.set(cacheKey, { result, createdAt: Date.now() });
|
|
1158
1316
|
return result;
|
|
@@ -1647,8 +1805,6 @@ function extractDeliverableURL(line) {
|
|
|
1647
1805
|
function configuredKBHosts() {
|
|
1648
1806
|
const cfg = loadDeliverableConfig();
|
|
1649
1807
|
const values = [
|
|
1650
|
-
process.env.KNOWLEDGE_DB_PUBLIC_URL,
|
|
1651
|
-
cfg.kbPublicUrl,
|
|
1652
1808
|
process.env.KNOWLEDGE_DB_URL,
|
|
1653
1809
|
cfg.kbBaseUrl,
|
|
1654
1810
|
DEFAULT_KB_BASE_URL,
|
|
@@ -1669,6 +1825,15 @@ function configuredKBHosts() {
|
|
|
1669
1825
|
return hosts;
|
|
1670
1826
|
}
|
|
1671
1827
|
|
|
1828
|
+
function isTrustedDeliverableURLPrefix(rawURL) {
|
|
1829
|
+
const normalized = String(rawURL || "").trim().toLowerCase();
|
|
1830
|
+
return (
|
|
1831
|
+
normalized.startsWith("https://uniclaw-ai-kb") ||
|
|
1832
|
+
normalized.startsWith("http://knowledge-db") ||
|
|
1833
|
+
normalized.startsWith("http://localhost")
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1672
1837
|
function isTrustedDeliverableURL(rawURL) {
|
|
1673
1838
|
if (!isString(rawURL) || !rawURL.trim()) {
|
|
1674
1839
|
return false;
|
|
@@ -1682,6 +1847,9 @@ function isTrustedDeliverableURL(rawURL) {
|
|
|
1682
1847
|
if (!DELIVERABLE_KB_PATH_RE.test(parsed.pathname)) {
|
|
1683
1848
|
return false;
|
|
1684
1849
|
}
|
|
1850
|
+
if (isTrustedDeliverableURLPrefix(rawURL)) {
|
|
1851
|
+
return true;
|
|
1852
|
+
}
|
|
1685
1853
|
const hosts = configuredKBHosts();
|
|
1686
1854
|
const host = parsed.host.toLowerCase();
|
|
1687
1855
|
const hostname = parsed.hostname.toLowerCase();
|
|
@@ -1814,7 +1982,11 @@ function extractDeliverableIdentity(rawURL) {
|
|
|
1814
1982
|
return "";
|
|
1815
1983
|
}
|
|
1816
1984
|
const docId = parsed.searchParams.get("id");
|
|
1817
|
-
|
|
1985
|
+
if (docId) {
|
|
1986
|
+
return String(docId);
|
|
1987
|
+
}
|
|
1988
|
+
const match = parsed.pathname.match(/\/documents\/(?:content|preview)\/([^/]+)/u);
|
|
1989
|
+
return match && match[1] ? decodeURIComponent(match[1]) : "";
|
|
1818
1990
|
}
|
|
1819
1991
|
|
|
1820
1992
|
function selectPalzFileURL(linkItems) {
|
|
@@ -1831,8 +2003,8 @@ function selectPalzFileURL(linkItems) {
|
|
|
1831
2003
|
if (identities.size !== 1) {
|
|
1832
2004
|
return "";
|
|
1833
2005
|
}
|
|
1834
|
-
const
|
|
1835
|
-
return (
|
|
2006
|
+
const preview = linkItems.find((item) => /预览链接/u.test(String((item && item.line) || "")));
|
|
2007
|
+
return (preview && preview.url) || linkItems[0].url || "";
|
|
1836
2008
|
}
|
|
1837
2009
|
|
|
1838
2010
|
function cloneBody(body, content, suffix) {
|
|
@@ -2060,6 +2232,7 @@ const plugin = {
|
|
|
2060
2232
|
|
|
2061
2233
|
api.on("before_tool_call", async (event) => {
|
|
2062
2234
|
if (isDeliverablesUploadTool(event.toolName)) {
|
|
2235
|
+
enrichDeliverableGroupContext(event);
|
|
2063
2236
|
cacheUploadSummary(event.params);
|
|
2064
2237
|
return;
|
|
2065
2238
|
}
|
|
@@ -2108,10 +2281,11 @@ plugin.__test = {
|
|
|
2108
2281
|
isOSSLikeURL,
|
|
2109
2282
|
isTrustedDeliverableURL,
|
|
2110
2283
|
kbExtractDocumentId,
|
|
2284
|
+
kbExtractReturnedLinks,
|
|
2111
2285
|
kbMultipartUpload,
|
|
2112
|
-
kbPreviewURL,
|
|
2113
2286
|
kbUploadPath,
|
|
2114
2287
|
loadDeliverableConfig,
|
|
2288
|
+
requireKBReturnedLink,
|
|
2115
2289
|
selectPalzFileURL,
|
|
2116
2290
|
splitDeliverableMessage,
|
|
2117
2291
|
};
|
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.1.
|
|
5
|
+
"version": "1.1.6",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -28,25 +28,25 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
28
28
|
|
|
29
29
|
| 参数 | 必填 | 取值来源 | 示例 |
|
|
30
30
|
|------|------|---------|------|
|
|
31
|
-
| `user_id` |
|
|
31
|
+
| `user_id` | **是(必传)** | 单聊取 `owner_id`,群聊取 `sender_id`。**不可省略**,否则 KB 上传报错 13002 | `cbb0fab9...` |
|
|
32
32
|
| `group_id` | 群聊时必填 | 群聊取消息元数据中的 `group_id`;**单聊不传**(自动为 `default`) | `grp_43c75713` |
|
|
33
33
|
| `group_name` | 群聊时必填 | 群聊取消息元数据中的群聊名称;**单聊不传** | `项目讨论群` |
|
|
34
|
-
| `type` | 是 | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`pdf`/`zip
|
|
34
|
+
| `type` | 是 | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`pdf`/`zip`;`link` 仅用于外部 `http(s)` URL | `article` |
|
|
35
35
|
| `file_name` | 是 | 有意义的文件名,单文件必须含扩展名;若用户未指定文档格式,默认用 `.md` | `report-2026.html` |
|
|
36
|
-
| `content_text` | 按需 | 文件的完整文本内容(HTML/Markdown
|
|
36
|
+
| `content_text` | 按需 | 文件的完整文本内容(HTML/Markdown等) | `<html>...</html>` |
|
|
37
37
|
| `file_path` | 按需 | PDF/PPT/图片/zip 等二进制文件的本地路径,推荐使用,避免手工复制 base64 | `output/sample.pdf` |
|
|
38
38
|
|
|
39
39
|
> **单聊(direct chat)**:`user_id` 填 `owner_id`,不传 `group_id` 和 `group_name`。
|
|
40
40
|
> **群聊(group chat)**:`user_id` 填 `sender_id`,`group_id` 和 `group_name` 从群聊元数据获取。
|
|
41
|
+
> **⚠ 警告**:`user_id` 必须传入真实值。当 `group_id` 是真实群 ID 但 `user_id` 缺失或为 `"default"` 时,KB 后端会返回 `500: 用户不存在`。
|
|
41
42
|
|
|
42
43
|
## 二进制文件上传(强制)
|
|
43
44
|
|
|
44
|
-
-
|
|
45
|
-
- 已经生成好的 PDF、PPT、图片、视频、zip 等二进制文件必须优先传 `file_path`,不要把 `base64` 命令输出复制到 `content_base64`。
|
|
45
|
+
- PDF、PPT、图片、视频、zip 等二进制文件必须优先传 `file_path`,不要把 `base64` 命令输出复制到 `content_base64`。
|
|
46
46
|
- `file_path` 指向你已经写入 `output/` 的文件,例如 `output/sample.pdf`;上传工具会读取文件并自动编码。
|
|
47
|
-
-
|
|
48
|
-
- 用户要求 PDF 时,必须上传 `.pdf` 交付物,`type` 使用 `pdf`;不要只返回 HTML/Markdown 作为替代格式。
|
|
47
|
+
- 用户要求 PDF 时,必须上传生成好的 `.pdf` 文件,`type` 使用 `pdf`;不要只返回 HTML/Markdown 作为替代格式。
|
|
49
48
|
- 如果上传工具返回错误,必须修正参数后重试,或明确告诉用户上传失败;禁止编造 OSS、下载或预览链接。
|
|
49
|
+
- 本地文件名或相对路径(例如 `review.html`、`output/report.md`)不是外部链接,不能用 `type=link`;必须用 `file_path` 或 `content_text` 上传。
|
|
50
50
|
|
|
51
51
|
## 多文件(游戏)
|
|
52
52
|
|