@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 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 生成的文件上传到 claw-gateway 交付物系统,并返回可分享的预览/下载链接。
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
- 上传工具通过以下环境变量调用 claw-gateway:
38
+ 上传工具根据 `HELM_ENV` 环境变量加载对应配置文件:
38
39
 
39
- - `CLAW_GATEWAY_URL`
40
- - `CLAW_GATEWAY_PUBLIC_URL`
41
- - `CLAW_GATEWAY_API_KEY` `OPENCLAW_GATEWAY_API_KEY`
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
 
@@ -0,0 +1,4 @@
1
+ {
2
+ "kbBaseUrl": "http://knowledge-db:8080/api/v1",
3
+ "kbPublicUrl": "https://uniclaw-ai-kb.csaiagent.com/api/v1"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "kbBaseUrl": "http://knowledge-db:8080/api/v1",
3
+ "kbPublicUrl": "https://uniclaw-ai-kb.csagentai.com/api/v1"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "kbBaseUrl": "http://knowledge-db:8080/api/v1",
3
+ "kbPublicUrl": "https://uniclaw-ai-kb.csjkagent.com/api/v1"
4
+ }
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 DEFAULT_GATEWAY_PUBLIC_URL = "https://claw-gateway.csagentai.com";
15
- const DEFAULT_GATEWAY_INTERNAL_URL = "http://claw-gateway:8080";
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 DELIVERABLE_GATEWAY_PATH_RE = /^\/openclaw-gateway\/(?:output|preview)(?:\/|$)/u;
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: "当前会话所属的群聊 ID(可选)。",
152
+ description: "分组 ID。单聊不传或传 'default';群聊传群 ID。",
109
153
  },
110
- user_id: {
154
+ group_name: {
111
155
  type: "string",
112
- description: "请求交付物的用户 ID(可选)。",
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: ["resource_id", "type", "file_name"],
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
- return (
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", "conversation_id", "conversationId"]);
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 gatewayURL() {
562
- return (process.env.CLAW_GATEWAY_URL || DEFAULT_GATEWAY_INTERNAL_URL).replace(/\/$/, "");
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 gatewayPublicURL() {
566
- return (process.env.CLAW_GATEWAY_PUBLIC_URL || process.env.CLAW_GATEWAY_URL || DEFAULT_GATEWAY_PUBLIC_URL).replace(/\/$/, "");
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 apiKey() {
570
- return process.env.CLAW_GATEWAY_API_KEY || process.env.OPENCLAW_GATEWAY_API_KEY || "api-key-1";
633
+ function kbApiKey() {
634
+ return process.env.KNOWLEDGE_DB_API_KEY || DEFAULT_KB_API_KEY;
571
635
  }
572
636
 
573
- function deriveReleaseName() {
574
- let release = process.env.botID || process.env.OPENCLAW_RELEASE || process.env.BOT_ID || "";
575
- if (!release) {
576
- const tok = process.env.OPENCLAW_GATEWAY_TOKEN || "";
577
- if (tok.indexOf("oc-") === 0) {
578
- release = tok.slice(3);
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 release;
661
+ return null;
582
662
  }
583
663
 
584
- function httpJSONRequest(method, requestPath, body) {
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(gatewayURL() + requestPath);
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 bodyStr = body ? JSON.stringify(body) : "";
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": "application/json",
604
- "X-API-Key": apiKey(),
605
- "Content-Length": Buffer.byteLength(bodyStr),
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(`gateway ${res.statusCode}: non-JSON response: ${text.slice(0, 200)}`));
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(`gateway ${res.statusCode}: ${obj.message || text.slice(0, 200)}`));
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
- if (bodyStr) {
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 response = await httpJSONRequest("POST", "/openclaw-gateway/be/deliverables", body);
933
- const data = response.data || response;
934
- const previewURL = data.previewUrl || `${gatewayPublicURL()}/openclaw-gateway/preview/${data.uuid}`;
935
- const downloadURL = data.downloadUrl || previewURL;
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
- uuid: data.uuid,
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
- const requestBody = {
978
- resourceId,
979
- groupId: payloadGroupId(body),
980
- userId: payloadUserId(body),
981
- release: deriveReleaseName(),
982
- type: deliverableTypeForPath(candidate.path, stat.isDirectory()),
983
- fileName: candidate.fileName || path.basename(candidate.path),
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
- requestBody.files = files;
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
- requestBody.contentBase64 = fs.readFileSync(candidate.path).toString("base64");
1141
+ fileBuffers = [{ fileName, content: fs.readFileSync(candidate.path) }];
996
1142
  }
997
1143
 
998
- const response = await httpJSONRequest("POST", "/openclaw-gateway/be/deliverables", requestBody);
999
- const data = response.data || response;
1000
- const previewURL = data.previewUrl || `${gatewayPublicURL()}/openclaw-gateway/preview/${data.uuid}`;
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: requestBody.fileName,
1003
- uuid: data.uuid,
1152
+ fileName,
1153
+ document_id: documentId,
1004
1154
  previewURL,
1005
- downloadURL: data.downloadUrl || previewURL,
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 configuredGatewayHosts() {
1647
+ function configuredKBHosts() {
1648
+ const cfg = loadDeliverableConfig();
1498
1649
  const values = [
1499
- process.env.CLAW_GATEWAY_PUBLIC_URL,
1500
- process.env.CLAW_GATEWAY_URL,
1501
- DEFAULT_GATEWAY_PUBLIC_URL,
1502
- DEFAULT_GATEWAY_INTERNAL_URL,
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 (!DELIVERABLE_GATEWAY_PATH_RE.test(parsed.pathname)) {
1682
+ if (!DELIVERABLE_KB_PATH_RE.test(parsed.pathname)) {
1531
1683
  return false;
1532
1684
  }
1533
- const hosts = configuredGatewayHosts();
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 (!DELIVERABLE_GATEWAY_PATH_RE.test(parsed.pathname)) {
1813
+ if (!DELIVERABLE_KB_PATH_RE.test(parsed.pathname)) {
1662
1814
  return "";
1663
1815
  }
1664
- const parts = parsed.pathname.split("/").filter(Boolean);
1665
- const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
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
- "交付物上传未成功,已阻止发送未通过 gateway 交付物系统生成的 OSS 文件链接。请重新通过交付物上传工具上传后再发送。",
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
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plugin-deliverables",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "npm_package": "@dai_ming/plugin-deliverables",
5
5
  "description": "Deliverables plugin: native upload tool + skill + AGENTS rules for AI-generated file uploads",
6
6
  "skills": {
@@ -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.2",
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.2",
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
- | `resource_id` | 消息元数据中的 `resource_id` 字段 | `user_xxx_lobster_yyy` |
32
- | `group_id` | 消息元数据中的 `group_id` `conversation_id` | `group_abc123` |
33
- | `user_id` | 消息元数据中的 `sender_id` `owner_id` | `cbb0fab9...` |
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)时**:`group_id` 填 `conversation_id`,`user_id` `owner_id`。
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