@dai_ming/plugin-deliverables 1.1.2 → 1.1.4
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/INSTALL.md +1 -1
- package/README.md +16 -5
- package/deliverables.dev.config.json +4 -0
- package/deliverables.prod.config.json +4 -0
- package/deliverables.staging.config.json +4 -0
- package/index.js +237 -85
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/skills/deliverables/SKILL.md +13 -12
package/INSTALL.md
CHANGED
|
@@ -21,7 +21,7 @@ installPlugins:
|
|
|
21
21
|
1. `openclaw plugins info plugin-deliverables` 能看到插件已启用。
|
|
22
22
|
2. Agent 工具列表里存在 `deliverables__upload_deliverable`。
|
|
23
23
|
3. Pod 内不再出现 `node ...mcp-servers/deliverables.js` 进程。
|
|
24
|
-
4. 生成交付物时,工具返回 `reply_markdown`、`preview_url`、`download_url`。
|
|
24
|
+
4. 生成交付物时,工具返回 `reply_markdown`、`preview_url`、`download_url`、`document_id`。
|
|
25
25
|
|
|
26
26
|
## 说明
|
|
27
27
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dai_ming/plugin-deliverables
|
|
2
2
|
|
|
3
|
-
OpenClaw Native 插件:注册 `deliverables__upload_deliverable` 原生 agent tool,把 AI
|
|
3
|
+
OpenClaw Native 插件:注册 `deliverables__upload_deliverable` 原生 agent tool,把 AI 生成的文件上传到知识库交付物系统,并返回可分享的预览/下载链接。
|
|
4
4
|
|
|
5
5
|
## 包含内容
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ OpenClaw Native 插件:注册 `deliverables__upload_deliverable` 原生 agent
|
|
|
11
11
|
| `openclaw-plugin.json` | gateway 兼容清单:声明 skill、AGENTS 规则和插件 entry |
|
|
12
12
|
| `skills/deliverables/SKILL.md` | Agent 使用交付物工具的技能说明 |
|
|
13
13
|
| `agents-rules/deliverables.md` | 注入到 workspace `AGENTS.md` 的硬规则 |
|
|
14
|
+
| `deliverables.*.config.json` | 各环境知识库连接配置 |
|
|
14
15
|
|
|
15
16
|
## 安装
|
|
16
17
|
|
|
@@ -34,11 +35,21 @@ installPlugins:
|
|
|
34
35
|
deliverables__upload_deliverable
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
上传工具根据 `HELM_ENV` 环境变量加载对应配置文件:
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
| HELM_ENV | 配置文件 | 说明 |
|
|
41
|
+
|----------|---------|------|
|
|
42
|
+
| (空) | `deliverables.config.json` | 本地开发(127.0.0.1) |
|
|
43
|
+
| `dev` | `deliverables.dev.config.json` | dev 环境 |
|
|
44
|
+
| `staging` | `deliverables.staging.config.json` | staging 环境 |
|
|
45
|
+
| `prod` | `deliverables.prod.config.json` | 生产环境 |
|
|
46
|
+
|
|
47
|
+
配置字段:
|
|
48
|
+
|
|
49
|
+
- `kbBaseUrl` — 知识库 API 内部地址(Pod 内通信)
|
|
50
|
+
- `kbPublicUrl` — 用户可见的预览链接地址
|
|
51
|
+
|
|
52
|
+
环境变量 `KNOWLEDGE_DB_URL` / `KNOWLEDGE_DB_PUBLIC_URL` 可覆盖配置文件值。`KNOWLEDGE_DB_API_KEY` 仅通过环境变量配置(默认 `kb-agent-key-2025`)。
|
|
42
53
|
|
|
43
54
|
## 发布
|
|
44
55
|
|
package/index.js
CHANGED
|
@@ -11,14 +11,50 @@ const PENDING_FILES_KEY = "__plugin_deliverables_pending_files__";
|
|
|
11
11
|
const UPLOAD_CACHE_KEY = "__plugin_deliverables_upload_cache__";
|
|
12
12
|
const SUMMARY_CACHE_LIMIT = 200;
|
|
13
13
|
const SHORT_SUMMARY_THRESHOLD = 120;
|
|
14
|
-
const
|
|
15
|
-
const
|
|
14
|
+
const DEFAULT_KB_BASE_URL = "http://knowledge-db:8080/api/v1";
|
|
15
|
+
const DEFAULT_KB_API_KEY = "kb-agent-key-2025";
|
|
16
|
+
const DEFAULT_GROUP_ID = "default";
|
|
17
|
+
const DEFAULT_GROUP_NAME = "我的文件夹";
|
|
18
|
+
const DEFAULT_DELIVERABLES_CONFIG_FILENAME = "deliverables.config.json";
|
|
19
|
+
let _cachedDeliverableConfig = null;
|
|
20
|
+
|
|
21
|
+
function getDeliverableConfigFilename() {
|
|
22
|
+
const helmEnv = (process.env.HELM_ENV || "").trim();
|
|
23
|
+
return helmEnv
|
|
24
|
+
? "deliverables." + helmEnv + ".config.json"
|
|
25
|
+
: DEFAULT_DELIVERABLES_CONFIG_FILENAME;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadDeliverableConfig() {
|
|
29
|
+
if (_cachedDeliverableConfig) return _cachedDeliverableConfig;
|
|
30
|
+
const helmEnv = (process.env.HELM_ENV || "").trim();
|
|
31
|
+
const configFilename = getDeliverableConfigFilename();
|
|
32
|
+
const candidates = [
|
|
33
|
+
path.resolve(process.cwd(), configFilename),
|
|
34
|
+
path.resolve(__dirname, configFilename),
|
|
35
|
+
];
|
|
36
|
+
for (const filePath of candidates) {
|
|
37
|
+
try {
|
|
38
|
+
if (fs.existsSync(filePath)) {
|
|
39
|
+
_cachedDeliverableConfig = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
40
|
+
return _cachedDeliverableConfig;
|
|
41
|
+
}
|
|
42
|
+
} catch (_err) { /* ignore */ }
|
|
43
|
+
}
|
|
44
|
+
if (helmEnv) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"plugin-deliverables: HELM_ENV=\"" + helmEnv + "\" but config file \"" + configFilename + "\" not found, searched: " + candidates.join(", ")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
_cachedDeliverableConfig = {};
|
|
50
|
+
return _cachedDeliverableConfig;
|
|
51
|
+
}
|
|
16
52
|
const AUTO_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
|
17
53
|
const AUTO_UPLOAD_MAX_FILES = 8;
|
|
18
54
|
const AUTO_UPLOAD_MAX_BYTES = 200 * 1024 * 1024;
|
|
19
55
|
const AUTO_UPLOAD_SCAN_MAX_ENTRIES = 4000;
|
|
20
56
|
const AUTO_UPLOAD_SCAN_DEPTH = 5;
|
|
21
|
-
const
|
|
57
|
+
const DELIVERABLE_KB_PATH_RE = /\/documents\/content\b/u;
|
|
22
58
|
const DELIVERABLE_LINK_PREFIX_RE =
|
|
23
59
|
/^\s*(?:[-*+]\s+)?(?:预览链接|下载链接|文件列表|项目入口|在线预览|在线体验|目录链接)\s*[::]/u;
|
|
24
60
|
const DELIVERABLE_LINK_LABEL_RE =
|
|
@@ -101,15 +137,23 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
101
137
|
properties: {
|
|
102
138
|
resource_id: {
|
|
103
139
|
type: "string",
|
|
104
|
-
description: "当前聊天框/会话的唯一 ID
|
|
140
|
+
description: "当前聊天框/会话的唯一 ID,用于缓存去重。",
|
|
141
|
+
},
|
|
142
|
+
user_id: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "上传者用户 ID。单聊取 owner_id,群聊取 sender_id。",
|
|
145
|
+
},
|
|
146
|
+
owner_id: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "等同于 user_id,从消息元数据 owner_id 字段获取。两者任传其一即可。",
|
|
105
149
|
},
|
|
106
150
|
group_id: {
|
|
107
151
|
type: "string",
|
|
108
|
-
description: "
|
|
152
|
+
description: "分组 ID。单聊不传或传 'default';群聊传群 ID。",
|
|
109
153
|
},
|
|
110
|
-
|
|
154
|
+
group_name: {
|
|
111
155
|
type: "string",
|
|
112
|
-
description: "
|
|
156
|
+
description: "群聊名称,群聊场景必填,用于构造知识库存储路径;单聊不传。",
|
|
113
157
|
},
|
|
114
158
|
type: {
|
|
115
159
|
type: "string",
|
|
@@ -146,7 +190,7 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
146
190
|
},
|
|
147
191
|
},
|
|
148
192
|
},
|
|
149
|
-
required: ["
|
|
193
|
+
required: ["type", "file_name"],
|
|
150
194
|
},
|
|
151
195
|
};
|
|
152
196
|
|
|
@@ -230,10 +274,14 @@ function deliverableTypeForPath(filePath, isDirectory) {
|
|
|
230
274
|
|
|
231
275
|
function isAllowedWorkspacePath(candidatePath) {
|
|
232
276
|
const normalized = normalizeSlash(path.resolve(candidatePath));
|
|
233
|
-
|
|
277
|
+
if (
|
|
234
278
|
normalized.indexOf("/data/workspace-") === 0 ||
|
|
235
279
|
normalized.indexOf("/home/node/.openclaw/workspace") === 0
|
|
236
|
-
)
|
|
280
|
+
) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
const openclawRoot = normalizeSlash(path.resolve(__dirname, "..", ".."));
|
|
284
|
+
return normalized.indexOf(openclawRoot + "/workspace") === 0;
|
|
237
285
|
}
|
|
238
286
|
|
|
239
287
|
function shouldIgnorePath(candidatePath) {
|
|
@@ -275,6 +323,20 @@ function workspaceRoots() {
|
|
|
275
323
|
} catch (_err) {
|
|
276
324
|
// /data is not guaranteed in non-pod test environments.
|
|
277
325
|
}
|
|
326
|
+
const openclawRoot = path.resolve(__dirname, "..", "..");
|
|
327
|
+
try {
|
|
328
|
+
for (const entry of fs.readdirSync(openclawRoot)) {
|
|
329
|
+
if (!entry || entry.indexOf("workspace") !== 0) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const root = path.join(openclawRoot, entry);
|
|
333
|
+
if (safeStat(root) && roots.indexOf(root) === -1) {
|
|
334
|
+
roots.push(root);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (_err) {
|
|
338
|
+
// local dev path may not exist
|
|
339
|
+
}
|
|
278
340
|
return roots;
|
|
279
341
|
}
|
|
280
342
|
|
|
@@ -491,7 +553,7 @@ function payloadResourceId(body) {
|
|
|
491
553
|
}
|
|
492
554
|
|
|
493
555
|
function payloadGroupId(body) {
|
|
494
|
-
return extractStringField(body, ["group_id", "groupId"
|
|
556
|
+
return extractStringField(body, ["group_id", "groupId"]);
|
|
495
557
|
}
|
|
496
558
|
|
|
497
559
|
function payloadUserId(body) {
|
|
@@ -558,51 +620,128 @@ function pendingCandidatesForPayload(body) {
|
|
|
558
620
|
return out;
|
|
559
621
|
}
|
|
560
622
|
|
|
561
|
-
function
|
|
562
|
-
|
|
623
|
+
function kbBaseURL() {
|
|
624
|
+
const cfg = loadDeliverableConfig();
|
|
625
|
+
return (process.env.KNOWLEDGE_DB_URL || cfg.kbBaseUrl || DEFAULT_KB_BASE_URL).replace(/\/$/, "");
|
|
563
626
|
}
|
|
564
627
|
|
|
565
|
-
function
|
|
566
|
-
|
|
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(/\/$/, "");
|
|
567
631
|
}
|
|
568
632
|
|
|
569
|
-
function
|
|
570
|
-
return process.env.
|
|
633
|
+
function kbApiKey() {
|
|
634
|
+
return process.env.KNOWLEDGE_DB_API_KEY || DEFAULT_KB_API_KEY;
|
|
571
635
|
}
|
|
572
636
|
|
|
573
|
-
function
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
637
|
+
function kbUploadPath(groupId, groupName) {
|
|
638
|
+
const gid = trimString(groupId) || DEFAULT_GROUP_ID;
|
|
639
|
+
const gname = gid === DEFAULT_GROUP_ID
|
|
640
|
+
? DEFAULT_GROUP_NAME
|
|
641
|
+
: trimString(groupName) || gid;
|
|
642
|
+
return "/" + gname + "/raw";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function kbPreviewURL(documentId, userId, groupId) {
|
|
646
|
+
return kbPublicURL() + "/documents/content?user_id="
|
|
647
|
+
+ encodeURIComponent(trimString(userId) || "default")
|
|
648
|
+
+ "&group_id=" + encodeURIComponent(trimString(groupId) || DEFAULT_GROUP_ID)
|
|
649
|
+
+ "&id=" + encodeURIComponent(String(documentId));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function kbExtractDocumentId(response) {
|
|
653
|
+
const data = (response && response.data) || response || {};
|
|
654
|
+
const items = Array.isArray(data.items) ? data.items : [];
|
|
655
|
+
if (items.length > 0 && items[0].document && items[0].document.id !== undefined) {
|
|
656
|
+
return items[0].document.id;
|
|
657
|
+
}
|
|
658
|
+
if (data.id !== undefined) {
|
|
659
|
+
return data.id;
|
|
580
660
|
}
|
|
581
|
-
return
|
|
661
|
+
return null;
|
|
582
662
|
}
|
|
583
663
|
|
|
584
|
-
function
|
|
664
|
+
function buildFileBuffersFromBody(body, isDirectory) {
|
|
665
|
+
if (isDirectory && Array.isArray(body.files)) {
|
|
666
|
+
return body.files.map((file) => ({
|
|
667
|
+
fileName: file.name || "file",
|
|
668
|
+
content: file.contentText
|
|
669
|
+
? Buffer.from(file.contentText, "utf8")
|
|
670
|
+
: Buffer.from(file.contentBase64 || "", "base64"),
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
const content = body.contentBase64
|
|
674
|
+
? Buffer.from(body.contentBase64, "base64")
|
|
675
|
+
: Buffer.from(body.contentText || "", "utf8");
|
|
676
|
+
return [{ fileName: body.fileName || "file", content }];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function kbMultipartUpload(fileBuffers, fields, pathsArray) {
|
|
585
680
|
return new Promise((resolve, reject) => {
|
|
586
681
|
let parsed;
|
|
587
682
|
try {
|
|
588
|
-
parsed = new URL(
|
|
683
|
+
parsed = new URL(kbBaseURL() + "/documents/batch-upload");
|
|
589
684
|
} catch (err) {
|
|
590
685
|
reject(err);
|
|
591
686
|
return;
|
|
592
687
|
}
|
|
688
|
+
const boundary = "----Deliverables" + Date.now() + Math.random().toString(36).slice(2);
|
|
689
|
+
const parts = [];
|
|
690
|
+
|
|
691
|
+
for (const file of fileBuffers) {
|
|
692
|
+
parts.push(
|
|
693
|
+
Buffer.from(
|
|
694
|
+
"--" + boundary + "\r\n" +
|
|
695
|
+
'Content-Disposition: form-data; name="files"; filename="' + file.fileName + '"\r\n' +
|
|
696
|
+
"Content-Type: application/octet-stream\r\n\r\n",
|
|
697
|
+
),
|
|
698
|
+
);
|
|
699
|
+
parts.push(file.content);
|
|
700
|
+
parts.push(Buffer.from("\r\n"));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (Array.isArray(pathsArray) && pathsArray.length > 0) {
|
|
704
|
+
for (const p of pathsArray) {
|
|
705
|
+
parts.push(
|
|
706
|
+
Buffer.from(
|
|
707
|
+
"--" + boundary + "\r\n" +
|
|
708
|
+
'Content-Disposition: form-data; name="paths"\r\n\r\n' +
|
|
709
|
+
(p || "") + "\r\n",
|
|
710
|
+
),
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
716
|
+
if (value === undefined || value === null || value === "") {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
parts.push(
|
|
720
|
+
Buffer.from(
|
|
721
|
+
"--" + boundary + "\r\n" +
|
|
722
|
+
'Content-Disposition: form-data; name="' + key + '"\r\n\r\n' +
|
|
723
|
+
String(value) + "\r\n",
|
|
724
|
+
),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
parts.push(Buffer.from("--" + boundary + "--\r\n"));
|
|
729
|
+
const body = Buffer.concat(parts);
|
|
730
|
+
|
|
593
731
|
const isHTTPS = parsed.protocol === "https:";
|
|
594
732
|
const transport = isHTTPS ? https : http;
|
|
595
|
-
const
|
|
733
|
+
const userId = (fields && fields.user_id) || "default";
|
|
596
734
|
const req = transport.request(
|
|
597
735
|
{
|
|
598
736
|
hostname: parsed.hostname,
|
|
599
737
|
port: parsed.port || (isHTTPS ? 443 : 80),
|
|
600
738
|
path: parsed.pathname + (parsed.search || ""),
|
|
601
|
-
method,
|
|
739
|
+
method: "POST",
|
|
602
740
|
headers: {
|
|
603
|
-
"Content-Type": "
|
|
604
|
-
"X-
|
|
605
|
-
"
|
|
741
|
+
"Content-Type": "multipart/form-data; boundary=" + boundary,
|
|
742
|
+
"X-User-ID": userId,
|
|
743
|
+
"X-API-Key": kbApiKey(),
|
|
744
|
+
"Content-Length": body.length,
|
|
606
745
|
},
|
|
607
746
|
},
|
|
608
747
|
(res) => {
|
|
@@ -614,11 +753,15 @@ function httpJSONRequest(method, requestPath, body) {
|
|
|
614
753
|
try {
|
|
615
754
|
obj = JSON.parse(text);
|
|
616
755
|
} catch (_err) {
|
|
617
|
-
reject(new Error(`
|
|
756
|
+
reject(new Error(`kb ${res.statusCode}: non-JSON response: ${text.slice(0, 200)}`));
|
|
618
757
|
return;
|
|
619
758
|
}
|
|
620
759
|
if (res.statusCode >= 400) {
|
|
621
|
-
reject(new Error(`
|
|
760
|
+
reject(new Error(`kb ${res.statusCode}: ${obj.message || text.slice(0, 200)}`));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (obj.code !== 0 && obj.code !== undefined && res.statusCode !== 206) {
|
|
764
|
+
reject(new Error(`kb API error: ${obj.message || text.slice(0, 200)}`));
|
|
622
765
|
return;
|
|
623
766
|
}
|
|
624
767
|
resolve(obj);
|
|
@@ -626,9 +769,7 @@ function httpJSONRequest(method, requestPath, body) {
|
|
|
626
769
|
},
|
|
627
770
|
);
|
|
628
771
|
req.on("error", reject);
|
|
629
|
-
|
|
630
|
-
req.write(bodyStr);
|
|
631
|
-
}
|
|
772
|
+
req.write(body);
|
|
632
773
|
req.end();
|
|
633
774
|
});
|
|
634
775
|
}
|
|
@@ -849,7 +990,7 @@ function buildReplyMarkdown(opts) {
|
|
|
849
990
|
if (previewURL) {
|
|
850
991
|
lines.push(`预览链接:[点击预览](${previewURL})`);
|
|
851
992
|
}
|
|
852
|
-
if (downloadURL) {
|
|
993
|
+
if (downloadURL && downloadURL !== previewURL) {
|
|
853
994
|
if (isDirectory || deliverableType === "game") {
|
|
854
995
|
lines.push(`文件列表:[查看目录](${downloadURL})`);
|
|
855
996
|
} else {
|
|
@@ -883,12 +1024,7 @@ function buildNativeUploadRequestBody(args) {
|
|
|
883
1024
|
}
|
|
884
1025
|
}
|
|
885
1026
|
|
|
886
|
-
const body = {
|
|
887
|
-
resourceId: args.resource_id,
|
|
888
|
-
groupId: args.group_id,
|
|
889
|
-
userId: args.user_id,
|
|
890
|
-
release: deriveReleaseName(),
|
|
891
|
-
};
|
|
1027
|
+
const body = {};
|
|
892
1028
|
|
|
893
1029
|
if (fileCandidate) {
|
|
894
1030
|
const stat = safeStat(fileCandidate.path);
|
|
@@ -929,10 +1065,20 @@ function buildNativeUploadRequestBody(args) {
|
|
|
929
1065
|
|
|
930
1066
|
async function uploadDeliverable(args) {
|
|
931
1067
|
const { body, isDirectory } = buildNativeUploadRequestBody(args || {});
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
const
|
|
935
|
-
const
|
|
1068
|
+
const userId = payloadUserId(args) || "default";
|
|
1069
|
+
const groupId = trimString(args && args.group_id) || DEFAULT_GROUP_ID;
|
|
1070
|
+
const groupName = trimString(args && args.group_name);
|
|
1071
|
+
const uploadPath = kbUploadPath(groupId, groupName);
|
|
1072
|
+
const fileBuffers = buildFileBuffersFromBody(body, isDirectory);
|
|
1073
|
+
const fields = {
|
|
1074
|
+
user_id: userId,
|
|
1075
|
+
group_id: groupId,
|
|
1076
|
+
path: uploadPath,
|
|
1077
|
+
};
|
|
1078
|
+
const response = await kbMultipartUpload(fileBuffers, fields);
|
|
1079
|
+
const documentId = kbExtractDocumentId(response);
|
|
1080
|
+
const previewURL = documentId ? kbPreviewURL(documentId, userId, groupId) : "";
|
|
1081
|
+
const downloadURL = previewURL;
|
|
936
1082
|
const replyMarkdown = buildReplyMarkdown({
|
|
937
1083
|
previewURL,
|
|
938
1084
|
downloadURL,
|
|
@@ -940,11 +1086,9 @@ async function uploadDeliverable(args) {
|
|
|
940
1086
|
isDirectory,
|
|
941
1087
|
});
|
|
942
1088
|
return {
|
|
943
|
-
|
|
944
|
-
backend: data.backend || "",
|
|
1089
|
+
document_id: documentId,
|
|
945
1090
|
download_url: downloadURL,
|
|
946
1091
|
preview_url: previewURL,
|
|
947
|
-
expire_at: data.expireAt,
|
|
948
1092
|
reply_markdown: replyMarkdown,
|
|
949
1093
|
message: replyMarkdown,
|
|
950
1094
|
};
|
|
@@ -974,35 +1118,41 @@ async function uploadCandidate(candidate, body) {
|
|
|
974
1118
|
if (!stat) {
|
|
975
1119
|
throw new Error(`file no longer exists: ${candidate.fileName}`);
|
|
976
1120
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
};
|
|
1121
|
+
|
|
1122
|
+
const userId = payloadUserId(body) || "default";
|
|
1123
|
+
const groupId = payloadGroupId(body) || DEFAULT_GROUP_ID;
|
|
1124
|
+
const fileName = candidate.fileName || path.basename(candidate.path);
|
|
1125
|
+
const uploadPath = kbUploadPath(groupId, "");
|
|
1126
|
+
|
|
1127
|
+
let fileBuffers;
|
|
985
1128
|
if (stat.isDirectory()) {
|
|
986
1129
|
const files = collectDirectoryFiles(candidate.path);
|
|
987
1130
|
if (files.length === 0) {
|
|
988
1131
|
throw new Error(`directory has no uploadable files: ${candidate.fileName}`);
|
|
989
1132
|
}
|
|
990
|
-
|
|
1133
|
+
fileBuffers = files.map((f) => ({
|
|
1134
|
+
fileName: f.name,
|
|
1135
|
+
content: f.contentText ? Buffer.from(f.contentText, "utf8") : Buffer.from(f.contentBase64 || "", "base64"),
|
|
1136
|
+
}));
|
|
991
1137
|
} else {
|
|
992
1138
|
if (stat.size > AUTO_UPLOAD_MAX_BYTES) {
|
|
993
1139
|
throw new Error(`file exceeds auto-upload limit: ${candidate.fileName}`);
|
|
994
1140
|
}
|
|
995
|
-
|
|
1141
|
+
fileBuffers = [{ fileName, content: fs.readFileSync(candidate.path) }];
|
|
996
1142
|
}
|
|
997
1143
|
|
|
998
|
-
const response = await
|
|
999
|
-
|
|
1000
|
-
|
|
1144
|
+
const response = await kbMultipartUpload(fileBuffers, {
|
|
1145
|
+
user_id: userId,
|
|
1146
|
+
group_id: groupId,
|
|
1147
|
+
path: uploadPath,
|
|
1148
|
+
});
|
|
1149
|
+
const documentId = kbExtractDocumentId(response);
|
|
1150
|
+
const previewURL = documentId ? kbPreviewURL(documentId, userId, groupId) : "";
|
|
1001
1151
|
const result = {
|
|
1002
|
-
fileName
|
|
1003
|
-
|
|
1152
|
+
fileName,
|
|
1153
|
+
document_id: documentId,
|
|
1004
1154
|
previewURL,
|
|
1005
|
-
downloadURL:
|
|
1155
|
+
downloadURL: previewURL,
|
|
1006
1156
|
};
|
|
1007
1157
|
cache.set(cacheKey, { result, createdAt: Date.now() });
|
|
1008
1158
|
return result;
|
|
@@ -1494,12 +1644,14 @@ function extractDeliverableURL(line) {
|
|
|
1494
1644
|
return "";
|
|
1495
1645
|
}
|
|
1496
1646
|
|
|
1497
|
-
function
|
|
1647
|
+
function configuredKBHosts() {
|
|
1648
|
+
const cfg = loadDeliverableConfig();
|
|
1498
1649
|
const values = [
|
|
1499
|
-
process.env.
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1650
|
+
process.env.KNOWLEDGE_DB_PUBLIC_URL,
|
|
1651
|
+
cfg.kbPublicUrl,
|
|
1652
|
+
process.env.KNOWLEDGE_DB_URL,
|
|
1653
|
+
cfg.kbBaseUrl,
|
|
1654
|
+
DEFAULT_KB_BASE_URL,
|
|
1503
1655
|
];
|
|
1504
1656
|
const hosts = new Set();
|
|
1505
1657
|
for (const value of values) {
|
|
@@ -1527,10 +1679,10 @@ function isTrustedDeliverableURL(rawURL) {
|
|
|
1527
1679
|
} catch (_err) {
|
|
1528
1680
|
return false;
|
|
1529
1681
|
}
|
|
1530
|
-
if (!
|
|
1682
|
+
if (!DELIVERABLE_KB_PATH_RE.test(parsed.pathname)) {
|
|
1531
1683
|
return false;
|
|
1532
1684
|
}
|
|
1533
|
-
const hosts =
|
|
1685
|
+
const hosts = configuredKBHosts();
|
|
1534
1686
|
const host = parsed.host.toLowerCase();
|
|
1535
1687
|
const hostname = parsed.hostname.toLowerCase();
|
|
1536
1688
|
return hosts.has(host) || hosts.has(hostname);
|
|
@@ -1658,17 +1810,11 @@ function extractDeliverableIdentity(rawURL) {
|
|
|
1658
1810
|
} catch (_err) {
|
|
1659
1811
|
return "";
|
|
1660
1812
|
}
|
|
1661
|
-
if (!
|
|
1813
|
+
if (!DELIVERABLE_KB_PATH_RE.test(parsed.pathname)) {
|
|
1662
1814
|
return "";
|
|
1663
1815
|
}
|
|
1664
|
-
const
|
|
1665
|
-
|
|
1666
|
-
for (const part of parts) {
|
|
1667
|
-
if (uuidRe.test(part)) {
|
|
1668
|
-
return part.toLowerCase();
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
return parsed.href;
|
|
1816
|
+
const docId = parsed.searchParams.get("id");
|
|
1817
|
+
return docId ? String(docId) : "";
|
|
1672
1818
|
}
|
|
1673
1819
|
|
|
1674
1820
|
function selectPalzFileURL(linkItems) {
|
|
@@ -1710,7 +1856,7 @@ function cloneBodyAsFileLink(body, fileUrl, suffix) {
|
|
|
1710
1856
|
function cloneBodyAsBlockedDeliverableLink(body) {
|
|
1711
1857
|
return cloneBody(
|
|
1712
1858
|
body,
|
|
1713
|
-
"
|
|
1859
|
+
"交付物上传未成功,已阻止发送未通过知识库交付物系统生成的文件链接。请重新通过交付物上传工具上传后再发送。",
|
|
1714
1860
|
"",
|
|
1715
1861
|
);
|
|
1716
1862
|
}
|
|
@@ -1951,15 +2097,21 @@ const plugin = {
|
|
|
1951
2097
|
};
|
|
1952
2098
|
|
|
1953
2099
|
plugin.__test = {
|
|
2100
|
+
buildFileBuffersFromBody,
|
|
1954
2101
|
buildNativeUploadRequestBody,
|
|
1955
2102
|
createBasicTextPDF,
|
|
1956
2103
|
deliverableTypeForPath,
|
|
1957
|
-
extractFileReferencesFromText,
|
|
1958
2104
|
extractDeliverableURL,
|
|
2105
|
+
extractFileReferencesFromText,
|
|
1959
2106
|
findUntrustedOSSDeliverableURL,
|
|
1960
2107
|
isDeliverableLinkLine,
|
|
1961
2108
|
isOSSLikeURL,
|
|
1962
2109
|
isTrustedDeliverableURL,
|
|
2110
|
+
kbExtractDocumentId,
|
|
2111
|
+
kbMultipartUpload,
|
|
2112
|
+
kbPreviewURL,
|
|
2113
|
+
kbUploadPath,
|
|
2114
|
+
loadDeliverableConfig,
|
|
1963
2115
|
selectPalzFileURL,
|
|
1964
2116
|
splitDeliverableMessage,
|
|
1965
2117
|
};
|
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.4",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dai_ming/plugin-deliverables",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "OpenClaw deliverables native plugin — upload AI-generated files to OSS and return shareable preview/download links",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openclaw",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"INSTALL.md",
|
|
17
17
|
"index.js",
|
|
18
|
+
"deliverables.*.config.json",
|
|
18
19
|
"openclaw.plugin.json",
|
|
19
20
|
"openclaw-plugin.json",
|
|
20
21
|
"skills/",
|
|
@@ -22,21 +22,22 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
22
22
|
- 如果你使用 `write` 工具创建交付物文件,目标路径必须在 `output/` 下。
|
|
23
23
|
- 如果你已经误写到工作区根目录,必须先移动或复制到 `output/`,再继续上传交付物并向用户汇报。
|
|
24
24
|
|
|
25
|
-
##
|
|
25
|
+
## 调用前必须准备的参数
|
|
26
26
|
|
|
27
27
|
从当前会话上下文提取以下值,**调用时一次性填完,不要留空**:
|
|
28
28
|
|
|
29
|
-
| 参数 | 取值来源 | 示例 |
|
|
30
|
-
|
|
31
|
-
| `
|
|
32
|
-
| `group_id` |
|
|
33
|
-
| `
|
|
34
|
-
| `type` | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`pdf`/`zip`/`link` | `article` |
|
|
35
|
-
| `file_name` | 有意义的文件名,单文件必须含扩展名;若用户未指定文档格式,默认用 `.md` | `report-2026.html` |
|
|
36
|
-
| `content_text` | 文件的完整文本内容(HTML/Markdown等);简单 PDF 可传文本/Markdown,由工具生成基础 PDF | `<html>...</html>` |
|
|
37
|
-
| `file_path` | PDF/PPT/图片/zip 等二进制文件的本地路径,推荐使用,避免手工复制 base64 | `output/sample.pdf` |
|
|
38
|
-
|
|
39
|
-
>
|
|
29
|
+
| 参数 | 必填 | 取值来源 | 示例 |
|
|
30
|
+
|------|------|---------|------|
|
|
31
|
+
| `user_id` | 是 | 单聊取 `owner_id`,群聊取 `sender_id` | `cbb0fab9...` |
|
|
32
|
+
| `group_id` | 群聊时必填 | 群聊取消息元数据中的 `group_id`;**单聊不传**(自动为 `default`) | `grp_43c75713` |
|
|
33
|
+
| `group_name` | 群聊时必填 | 群聊取消息元数据中的群聊名称;**单聊不传** | `项目讨论群` |
|
|
34
|
+
| `type` | 是 | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`pdf`/`zip`/`link` | `article` |
|
|
35
|
+
| `file_name` | 是 | 有意义的文件名,单文件必须含扩展名;若用户未指定文档格式,默认用 `.md` | `report-2026.html` |
|
|
36
|
+
| `content_text` | 按需 | 文件的完整文本内容(HTML/Markdown等);简单 PDF 可传文本/Markdown,由工具生成基础 PDF | `<html>...</html>` |
|
|
37
|
+
| `file_path` | 按需 | PDF/PPT/图片/zip 等二进制文件的本地路径,推荐使用,避免手工复制 base64 | `output/sample.pdf` |
|
|
38
|
+
|
|
39
|
+
> **单聊(direct chat)**:`user_id` 填 `owner_id`,不传 `group_id` 和 `group_name`。
|
|
40
|
+
> **群聊(group chat)**:`user_id` 填 `sender_id`,`group_id` 和 `group_name` 从群聊元数据获取。
|
|
40
41
|
|
|
41
42
|
## 二进制文件上传(强制)
|
|
42
43
|
|