@dai_ming/plugin-deliverables 1.2.4 → 1.2.5
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/agents-rules/deliverables.md +3 -4
- package/deliverables.dev.config.json +2 -1
- package/deliverables.prod.config.json +2 -1
- package/deliverables.staging.config.json +2 -1
- package/index.js +271 -273
- package/openclaw-plugin.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/deliverables/SKILL.md +10 -6
|
@@ -81,10 +81,9 @@ Tool name note:
|
|
|
81
81
|
Keep that format instead of forcing a zip link.
|
|
82
82
|
23. Do NOT only say "已保存到工作空间".
|
|
83
83
|
24. Do NOT append workspace path or a raw URL block after the Markdown links.
|
|
84
|
-
25.
|
|
85
|
-
26.
|
|
86
|
-
27.
|
|
87
|
-
28. 调用上传工具时 MUST 传入 `user_id`:单聊取 `owner_id`,群聊取 `sender_id`。漏传 `user_id` 会导致 KB 上传失败(error 13002: 用户不存在),尤其在同时传了真实 `group_id` 的场景下。
|
|
84
|
+
25. 调用上传工具时,MUST 原样传入会话元数据:`conversation_type`(`"direct"` 或 `"group"`)、`conversation_id`、`owner_id`、`sender_id`、`lobster_id`、`lobster_name`,照消息元数据/会话上下文填,不要自己加工。这些都是必填。插件会据此推导 `user_id` 与 `group_id`(单聊 → `owner_id` + 个人文件夹;群聊 → `sender_id` + 群)。
|
|
85
|
+
26. 群聊场景额外传 `group_id` 和 `group_name`(取自消息元数据);单聊不传。
|
|
86
|
+
27. 不要把单聊的 `conversation_id` 当 `group_id` 传——单聊请把 `group_id` 留空或传 `"default"`,否则 KB 会因"用户不在群内"上传失败。
|
|
88
87
|
|
|
89
88
|
### Exception
|
|
90
89
|
|
package/index.js
CHANGED
|
@@ -12,9 +12,10 @@ const UPLOAD_CACHE_KEY = "__plugin_deliverables_upload_cache__";
|
|
|
12
12
|
const SUMMARY_CACHE_LIMIT = 200;
|
|
13
13
|
const SHORT_SUMMARY_THRESHOLD = 120;
|
|
14
14
|
const DEFAULT_KB_BASE_URL = "http://knowledge-db:8080/api/v1";
|
|
15
|
-
|
|
15
|
+
// Deliverables now go through the kb-engine (classification + path building +
|
|
16
|
+
// async move) instead of calling ai-kb's batch-upload directly.
|
|
17
|
+
const DEFAULT_ENGINE_BASE_URL = "http://uniclaw-kb-engine:8070/uniclaw-kb-engine/api/v1";
|
|
16
18
|
const DEFAULT_GROUP_ID = "default";
|
|
17
|
-
const DEFAULT_GROUP_NAME = "我的文件夹";
|
|
18
19
|
const DEFAULT_DELIVERABLES_CONFIG_FILENAME = "deliverables.config.json";
|
|
19
20
|
let _cachedDeliverableConfig = null;
|
|
20
21
|
|
|
@@ -125,8 +126,8 @@ const RUNTIME_DELIVERABLES_GUIDANCE = [
|
|
|
125
126
|
"- 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.",
|
|
126
127
|
"- 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.",
|
|
127
128
|
"- 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:
|
|
129
|
-
"-
|
|
129
|
+
"- CRITICAL: Pass the raw conversation metadata to `deliverables__upload_deliverable` verbatim — `conversation_type` (\"direct\" or \"group\"), `conversation_id`, `owner_id`, `sender_id`, `lobster_id`, and `lobster_name` — exactly as they appear in the conversation context/message metadata. These are all required. The plugin derives `user_id` and `group_id` from them (direct → owner_id + personal folder; group → sender_id + the group), so you do NOT pick those yourself. In group chats you MUST also pass `group_id` and `group_name`. Never pass a single-chat `conversation_id` as `group_id` — it causes a KB upload failure (user is not a member of group).",
|
|
130
|
+
"- `lobster_name` is the deliverable's first-level isolation directory (keeps different agents' deliverables separated). Use the real `lobster_name` from conversation context/message metadata when present; do not invent one or fall back to release/resource ids.",
|
|
130
131
|
].join("\n");
|
|
131
132
|
|
|
132
133
|
const UPLOAD_DELIVERABLE_TOOL = {
|
|
@@ -139,21 +140,33 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
139
140
|
inputSchema: {
|
|
140
141
|
type: "object",
|
|
141
142
|
properties: {
|
|
142
|
-
|
|
143
|
+
conversation_type: {
|
|
143
144
|
type: "string",
|
|
144
|
-
description: "
|
|
145
|
+
description: "会话类型,从消息元数据 conversation_type 获取:单聊为 'direct',群聊为 'group'。决定 user_id 取值与是否按群归档。",
|
|
145
146
|
},
|
|
146
|
-
|
|
147
|
+
conversation_id: {
|
|
147
148
|
type: "string",
|
|
148
|
-
description: "
|
|
149
|
+
description: "会话 ID,从消息元数据 conversation_id 获取。群聊时作为 group_id 兜底;并用于缓存去重。",
|
|
149
150
|
},
|
|
150
151
|
owner_id: {
|
|
151
152
|
type: "string",
|
|
152
|
-
description: "
|
|
153
|
+
description: "会话归属用户 ID,从消息元数据 owner_id 获取。单聊上传者即 owner_id。",
|
|
154
|
+
},
|
|
155
|
+
sender_id: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "消息发送者 ID,从消息元数据 sender_id 获取。群聊上传者即 sender_id。",
|
|
158
|
+
},
|
|
159
|
+
lobster_name: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description: "产出该交付物的 agent 显示名,从会话上下文/消息元数据 lobster_name 获取;作为交付物的一级隔离目录。必填。",
|
|
162
|
+
},
|
|
163
|
+
lobster_id: {
|
|
164
|
+
type: "string",
|
|
165
|
+
description: "agent 稳定 ID,从消息元数据 lobster_id 获取。随交付物记录,预留按 ID 隔离。",
|
|
153
166
|
},
|
|
154
167
|
group_id: {
|
|
155
168
|
type: "string",
|
|
156
|
-
description: "分组 ID。单聊不传或传 'default';群聊传群 ID
|
|
169
|
+
description: "分组 ID。单聊不传或传 'default';群聊传群 ID(不传则用 conversation_id 兜底)。",
|
|
157
170
|
},
|
|
158
171
|
group_name: {
|
|
159
172
|
type: "string",
|
|
@@ -194,7 +207,16 @@ const UPLOAD_DELIVERABLE_TOOL = {
|
|
|
194
207
|
},
|
|
195
208
|
},
|
|
196
209
|
},
|
|
197
|
-
required: [
|
|
210
|
+
required: [
|
|
211
|
+
"type",
|
|
212
|
+
"file_name",
|
|
213
|
+
"conversation_type",
|
|
214
|
+
"conversation_id",
|
|
215
|
+
"owner_id",
|
|
216
|
+
"sender_id",
|
|
217
|
+
"lobster_name",
|
|
218
|
+
"lobster_id",
|
|
219
|
+
],
|
|
198
220
|
},
|
|
199
221
|
};
|
|
200
222
|
|
|
@@ -285,23 +307,37 @@ function deliverableTypeForPath(filePath, isDirectory) {
|
|
|
285
307
|
return "article";
|
|
286
308
|
}
|
|
287
309
|
|
|
310
|
+
function openclawRoot() {
|
|
311
|
+
return path.resolve(__dirname, "..", "..");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function workspaceTenantBases() {
|
|
315
|
+
return [
|
|
316
|
+
"/data/tenants",
|
|
317
|
+
process.env.OPENCLAW_HOME,
|
|
318
|
+
process.env.HOME ? path.join(process.env.HOME, ".openclaw") : "",
|
|
319
|
+
openclawRoot(),
|
|
320
|
+
].filter((base, index, all) => base && all.indexOf(base) === index);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isPathUnderTenantWorkspace(normalizedPath, normalizedBase) {
|
|
324
|
+
if (normalizedPath.indexOf(normalizedBase + "/") !== 0) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const relative = normalizedPath.slice(normalizedBase.length + 1);
|
|
328
|
+
const parts = relative.split("/").filter(Boolean);
|
|
329
|
+
return parts.length >= 2 && parts[1].indexOf("workspace-") === 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
288
332
|
function isAllowedWorkspacePath(candidatePath) {
|
|
289
333
|
const normalized = normalizeSlash(path.resolve(candidatePath));
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
normalized
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
const openclawRoot = normalizeSlash(path.resolve(__dirname, "..", ".."));
|
|
298
|
-
if (normalized.indexOf(openclawRoot + "/workspace") === 0) {
|
|
299
|
-
return true;
|
|
334
|
+
for (const base of workspaceTenantBases()) {
|
|
335
|
+
const normalizedBase = normalizeSlash(path.resolve(base));
|
|
336
|
+
if (isPathUnderTenantWorkspace(normalized, normalizedBase)) {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
300
339
|
}
|
|
301
|
-
|
|
302
|
-
// 目录段都允许,覆盖 /data/tenants/<user>/workspace-<lobster>、
|
|
303
|
-
// /home/node/.openclaw/<user>/workspace-<lobster> 等。
|
|
304
|
-
return /\/workspace-[^/]+(?:\/|$)/.test(normalized);
|
|
340
|
+
return false;
|
|
305
341
|
}
|
|
306
342
|
|
|
307
343
|
function shouldIgnorePath(candidatePath) {
|
|
@@ -325,28 +361,7 @@ async function safeStat(candidatePath) {
|
|
|
325
361
|
|
|
326
362
|
async function workspaceRoots() {
|
|
327
363
|
const roots = [];
|
|
328
|
-
for (const
|
|
329
|
-
if (await safeStat(root)) {
|
|
330
|
-
roots.push(root);
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
for (const entry of await fs.promises.readdir("/data")) {
|
|
335
|
-
if (!entry || entry.indexOf("workspace-") !== 0) {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
const root = path.join("/data", entry);
|
|
339
|
-
if (await safeStat(root)) {
|
|
340
|
-
roots.push(root);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} catch (_err) {
|
|
344
|
-
// /data is not guaranteed in non-pod test environments.
|
|
345
|
-
}
|
|
346
|
-
// 多租户布局:枚举 <base>/<tenant-or-user>/workspace-* 下的 workspace 根目录,
|
|
347
|
-
// 覆盖 /data/tenants/<user>/workspace-<lobster>、
|
|
348
|
-
// /home/node/.openclaw/<user>/workspace-<lobster> 等。
|
|
349
|
-
for (const base of ["/data/tenants", "/home/node/.openclaw"]) {
|
|
364
|
+
for (const base of workspaceTenantBases()) {
|
|
350
365
|
let tenants;
|
|
351
366
|
try {
|
|
352
367
|
tenants = await fs.promises.readdir(base);
|
|
@@ -369,26 +384,13 @@ async function workspaceRoots() {
|
|
|
369
384
|
continue;
|
|
370
385
|
}
|
|
371
386
|
const root = path.join(tenantDir, child);
|
|
372
|
-
|
|
387
|
+
const stat = await safeStat(root);
|
|
388
|
+
if (stat && stat.isDirectory() && roots.indexOf(root) === -1) {
|
|
373
389
|
roots.push(root);
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
}
|
|
377
393
|
}
|
|
378
|
-
const openclawRoot = path.resolve(__dirname, "..", "..");
|
|
379
|
-
try {
|
|
380
|
-
for (const entry of await fs.promises.readdir(openclawRoot)) {
|
|
381
|
-
if (!entry || entry.indexOf("workspace") !== 0) {
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
const root = path.join(openclawRoot, entry);
|
|
385
|
-
if ((await safeStat(root)) && roots.indexOf(root) === -1) {
|
|
386
|
-
roots.push(root);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
} catch (_err) {
|
|
390
|
-
// local dev path may not exist
|
|
391
|
-
}
|
|
392
394
|
return roots;
|
|
393
395
|
}
|
|
394
396
|
|
|
@@ -566,7 +568,22 @@ async function collectDirectoryFiles(dirPath) {
|
|
|
566
568
|
}
|
|
567
569
|
}
|
|
568
570
|
await scan(dirPath, AUTO_UPLOAD_SCAN_DEPTH);
|
|
569
|
-
return files;
|
|
571
|
+
return sortDeliverableFiles(files);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function deliverableFileSortKey(file) {
|
|
575
|
+
const name = normalizeSlash(file && file.name ? file.name : "").toLowerCase();
|
|
576
|
+
if (name === "index.html" || name === "index.htm") {
|
|
577
|
+
return "0:" + name;
|
|
578
|
+
}
|
|
579
|
+
if (path.basename(name) === "index.html" || path.basename(name) === "index.htm") {
|
|
580
|
+
return "1:" + name;
|
|
581
|
+
}
|
|
582
|
+
return "2:" + name;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function sortDeliverableFiles(files) {
|
|
586
|
+
return (files || []).slice().sort((a, b) => deliverableFileSortKey(a).localeCompare(deliverableFileSortKey(b)));
|
|
570
587
|
}
|
|
571
588
|
|
|
572
589
|
function extractStringField(obj, keys) {
|
|
@@ -603,8 +620,25 @@ function extractContextString(event, keys) {
|
|
|
603
620
|
return "";
|
|
604
621
|
}
|
|
605
622
|
|
|
606
|
-
|
|
607
|
-
|
|
623
|
+
// payloadConversationID is the per-conversation key used for cache dedup and for
|
|
624
|
+
// correlating the upload-tool-call event with the later message-send event
|
|
625
|
+
// (replaces the old resource_id). conversation_id is present in both contexts.
|
|
626
|
+
function payloadConversationID(body) {
|
|
627
|
+
return extractStringField(body, ["conversation_id", "conversationId", "resource_id", "resourceId"]);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// isGroupChat reports whether the context is a group chat, keyed on
|
|
631
|
+
// conversation_type (the authoritative field the runtime actually sends —
|
|
632
|
+
// "direct" / "group", matching the engine). Only in a group chat is a
|
|
633
|
+
// conversation_id a meaningful group_id; in a direct chat it is a per-user
|
|
634
|
+
// session id (conv_<user>_<lobster>) that is NOT a group and must never be used
|
|
635
|
+
// as group_id (ai-kb would reject it with "user is not a member of group").
|
|
636
|
+
// Absent conversation_type, default to direct (single chat) — the safe choice.
|
|
637
|
+
function isGroupChat(obj) {
|
|
638
|
+
if (!obj || typeof obj !== "object") {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
return extractStringField(obj, ["conversation_type", "conversationType"]).toLowerCase() === "group";
|
|
608
642
|
}
|
|
609
643
|
|
|
610
644
|
function payloadGroupId(body) {
|
|
@@ -612,7 +646,12 @@ function payloadGroupId(body) {
|
|
|
612
646
|
if (explicit) {
|
|
613
647
|
return explicit;
|
|
614
648
|
}
|
|
615
|
-
|
|
649
|
+
// Only fall back to the conversation id as a group id in a group chat; a direct
|
|
650
|
+
// chat must stay empty (-> engine treats it as the personal "我的文件夹" root).
|
|
651
|
+
if (!isGroupChat(body)) {
|
|
652
|
+
return "";
|
|
653
|
+
}
|
|
654
|
+
const fallback = extractStringField(body, ["conversation_id", "conversationId"]);
|
|
616
655
|
return fallback && fallback !== "default" ? fallback : "";
|
|
617
656
|
}
|
|
618
657
|
|
|
@@ -620,73 +659,76 @@ function enrichDeliverableGroupContext(event) {
|
|
|
620
659
|
if (!event || !event.params || typeof event.params !== "object") {
|
|
621
660
|
return;
|
|
622
661
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
]
|
|
628
|
-
|
|
629
|
-
|
|
662
|
+
// Populate the raw routing fields from the message context when the agent did
|
|
663
|
+
// not pass them. group_id / user_id are then DERIVED from these at request time
|
|
664
|
+
// (payloadGroupId / payloadUserId), so we do not need to compute them here.
|
|
665
|
+
const rawFields = [
|
|
666
|
+
["conversation_type", ["conversation_type", "conversationType"]],
|
|
667
|
+
["conversation_id", ["conversation_id", "conversationId"]],
|
|
668
|
+
["owner_id", ["owner_id", "ownerId"]],
|
|
669
|
+
["sender_id", ["sender_id", "senderId"]],
|
|
670
|
+
["group_id", ["group_id", "groupId"]],
|
|
671
|
+
];
|
|
672
|
+
for (const [target, keys] of rawFields) {
|
|
673
|
+
if (trimString(event.params[target])) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const inferred = extractContextString(event, keys);
|
|
677
|
+
if (inferred) {
|
|
678
|
+
event.params[target] = inferred;
|
|
630
679
|
}
|
|
631
680
|
}
|
|
632
681
|
const existingGroupName = trimString(event.params.group_name);
|
|
633
682
|
if (!existingGroupName) {
|
|
634
683
|
const inferredGroupName = extractContextString(event, [
|
|
635
|
-
"group_name", "groupName",
|
|
684
|
+
"group_name", "groupName",
|
|
636
685
|
]);
|
|
637
686
|
if (inferredGroupName) {
|
|
638
687
|
event.params.group_name = inferredGroupName;
|
|
639
688
|
}
|
|
640
689
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
690
|
+
// lobster_name is the deliverable's first-level isolation dir. Prefer the real
|
|
691
|
+
// value from params/context/metadata; do NOT fall back to release/resource ids —
|
|
692
|
+
// those yield wrong, unreadable directory names. A still-missing value is
|
|
693
|
+
// rejected by the engine, which is the correct signal.
|
|
694
|
+
const existingLobsterName = trimString(event.params.lobster_name);
|
|
695
|
+
if (!existingLobsterName) {
|
|
696
|
+
const inferredLobsterName = extractContextString(event, [
|
|
697
|
+
"lobster_name", "lobsterName", "agent_name", "agentName",
|
|
698
|
+
]);
|
|
699
|
+
if (inferredLobsterName) {
|
|
700
|
+
event.params.lobster_name = inferredLobsterName;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const existingLobsterId = trimString(event.params.lobster_id);
|
|
704
|
+
if (!existingLobsterId) {
|
|
705
|
+
const inferredLobsterId = extractContextString(event, [
|
|
706
|
+
"lobster_id", "lobsterId", "agent_id", "agentId",
|
|
645
707
|
]);
|
|
646
|
-
if (
|
|
647
|
-
event.params.
|
|
708
|
+
if (inferredLobsterId) {
|
|
709
|
+
event.params.lobster_id = inferredLobsterId;
|
|
648
710
|
}
|
|
649
711
|
}
|
|
650
712
|
}
|
|
651
713
|
|
|
714
|
+
// payloadUserId resolves the uploader id from conversation_type — direct chats
|
|
715
|
+
// upload as owner_id, group chats as sender_id (matching the deliverables
|
|
716
|
+
// guidance). Upstream never sends a `user_id`; it is always derived here. Each
|
|
717
|
+
// branch falls back to the other id so a partially-populated payload still
|
|
718
|
+
// resolves something.
|
|
652
719
|
function payloadUserId(body) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
function normalizeBareUserID(value) {
|
|
657
|
-
let raw = trimString(value);
|
|
658
|
-
if (!raw) {
|
|
659
|
-
return "";
|
|
720
|
+
if (isGroupChat(body)) {
|
|
721
|
+
return extractStringField(body, ["sender_id", "senderId", "owner_id", "ownerId"]);
|
|
660
722
|
}
|
|
661
|
-
|
|
662
|
-
raw = raw.slice("user_".length, raw.indexOf("_lobster_"));
|
|
663
|
-
} else if (raw.indexOf("user-") === 0 || raw.indexOf("user_") === 0) {
|
|
664
|
-
raw = raw.slice("user-".length);
|
|
665
|
-
}
|
|
666
|
-
return raw;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function userIDFromRelease(release) {
|
|
670
|
-
return normalizeBareUserID(release);
|
|
723
|
+
return extractStringField(body, ["owner_id", "ownerId", "sender_id", "senderId"]);
|
|
671
724
|
}
|
|
672
725
|
|
|
673
|
-
function
|
|
674
|
-
|
|
675
|
-
if (raw.indexOf("user_") !== 0 && raw.indexOf("user-") !== 0) {
|
|
676
|
-
return "";
|
|
677
|
-
}
|
|
678
|
-
return normalizeBareUserID(raw);
|
|
726
|
+
function payloadLobsterName(body) {
|
|
727
|
+
return extractStringField(body, ["lobster_name", "lobsterName", "agent_name", "agentName"]);
|
|
679
728
|
}
|
|
680
729
|
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
if (!release) {
|
|
684
|
-
const tok = process.env.OPENCLAW_GATEWAY_TOKEN || "";
|
|
685
|
-
if (tok.indexOf("oc-") === 0) {
|
|
686
|
-
release = tok.slice(3);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
return release;
|
|
730
|
+
function payloadLobsterId(body) {
|
|
731
|
+
return extractStringField(body, ["lobster_id", "lobsterId", "agent_id", "agentId"]);
|
|
690
732
|
}
|
|
691
733
|
|
|
692
734
|
function isWriteTool(toolName) {
|
|
@@ -719,25 +761,25 @@ function rememberPendingFile(event) {
|
|
|
719
761
|
}
|
|
720
762
|
const store = getPendingFileStore();
|
|
721
763
|
pruneTimedMap(store, AUTO_UPLOAD_TTL_MS);
|
|
722
|
-
const
|
|
723
|
-
const key = `${
|
|
764
|
+
const conversationId = extractContextString(event, ["conversation_id", "conversationId", "resource_id", "resourceId"]);
|
|
765
|
+
const key = `${conversationId || "unknown"}:${writePath}`;
|
|
724
766
|
store.set(key, {
|
|
725
767
|
path: writePath,
|
|
726
|
-
|
|
768
|
+
conversationId,
|
|
727
769
|
createdAt: Date.now(),
|
|
728
770
|
});
|
|
729
771
|
}
|
|
730
772
|
|
|
731
773
|
async function pendingCandidatesForPayload(body) {
|
|
732
|
-
const
|
|
733
|
-
if (!
|
|
774
|
+
const conversationId = payloadConversationID(body);
|
|
775
|
+
if (!conversationId) {
|
|
734
776
|
return [];
|
|
735
777
|
}
|
|
736
778
|
const store = getPendingFileStore();
|
|
737
779
|
pruneTimedMap(store, AUTO_UPLOAD_TTL_MS);
|
|
738
780
|
const out = [];
|
|
739
781
|
for (const entry of store.values()) {
|
|
740
|
-
if (!entry || entry.
|
|
782
|
+
if (!entry || entry.conversationId !== conversationId) {
|
|
741
783
|
continue;
|
|
742
784
|
}
|
|
743
785
|
const ref = { raw: entry.path, value: entry.path, kind: "pending" };
|
|
@@ -749,97 +791,14 @@ async function pendingCandidatesForPayload(body) {
|
|
|
749
791
|
return out;
|
|
750
792
|
}
|
|
751
793
|
|
|
752
|
-
function
|
|
794
|
+
function engineBaseURL() {
|
|
753
795
|
const cfg = loadDeliverableConfig();
|
|
754
|
-
return (process.env.
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function kbApiKey() {
|
|
758
|
-
return process.env.KNOWLEDGE_DB_API_KEY || DEFAULT_KB_API_KEY;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function kbUploadPath(groupId, groupName) {
|
|
762
|
-
const gid = trimString(groupId) || DEFAULT_GROUP_ID;
|
|
763
|
-
const gname = gid === DEFAULT_GROUP_ID
|
|
764
|
-
? DEFAULT_GROUP_NAME
|
|
765
|
-
: trimString(groupName) || gid;
|
|
766
|
-
return "/" + gname + "/raw/交付物";
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function kbUploadItems(response) {
|
|
770
|
-
const data = (response && response.data) || response || {};
|
|
771
|
-
return Array.isArray(data.items) ? data.items : [];
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function kbFirstSuccessfulUploadItem(response) {
|
|
775
|
-
const items = kbUploadItems(response);
|
|
776
|
-
if (items.length === 0) {
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
return items.find((item) => item && !item.error && item.document) || items[0] || null;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function kbExtractDocument(response) {
|
|
783
|
-
const item = kbFirstSuccessfulUploadItem(response);
|
|
784
|
-
if (item && item.document && typeof item.document === "object" && !Array.isArray(item.document)) {
|
|
785
|
-
return item.document;
|
|
786
|
-
}
|
|
787
|
-
const data = (response && response.data) || response || {};
|
|
788
|
-
if (data.document && typeof data.document === "object" && !Array.isArray(data.document)) {
|
|
789
|
-
return data.document;
|
|
790
|
-
}
|
|
791
|
-
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function kbExtractDocumentId(response) {
|
|
795
|
-
const document = kbExtractDocument(response);
|
|
796
|
-
if (document.id !== undefined) {
|
|
797
|
-
return document.id;
|
|
798
|
-
}
|
|
799
|
-
const data = (response && response.data) || response || {};
|
|
800
|
-
if (data.id !== undefined) {
|
|
801
|
-
return data.id;
|
|
802
|
-
}
|
|
803
|
-
return null;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
function extractURLField(obj, keys) {
|
|
807
|
-
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
808
|
-
return "";
|
|
809
|
-
}
|
|
810
|
-
for (const key of keys) {
|
|
811
|
-
const value = obj[key];
|
|
812
|
-
if (isString(value) && value.trim()) {
|
|
813
|
-
return value.trim();
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
return "";
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function kbExtractReturnedLinks(response) {
|
|
820
|
-
const item = kbFirstSuccessfulUploadItem(response) || {};
|
|
821
|
-
const document = kbExtractDocument(response);
|
|
822
|
-
return {
|
|
823
|
-
downloadURL:
|
|
824
|
-
extractURLField(document, ["download_url", "downloadURL"]) ||
|
|
825
|
-
extractURLField(item, ["download_url", "downloadURL"]),
|
|
826
|
-
previewURL:
|
|
827
|
-
extractURLField(document, ["preview_url", "previewURL"]) ||
|
|
828
|
-
extractURLField(item, ["preview_url", "previewURL"]),
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function requireKBReturnedLink(response) {
|
|
833
|
-
const links = kbExtractReturnedLinks(response);
|
|
834
|
-
if (!links.previewURL && !links.downloadURL) {
|
|
835
|
-
throw new Error("kb upload response missing preview_url/download_url");
|
|
836
|
-
}
|
|
837
|
-
return links;
|
|
796
|
+
return (process.env.UNICLAW_KB_ENGINE_URL || cfg.engineBaseUrl || DEFAULT_ENGINE_BASE_URL).replace(/\/$/, "");
|
|
838
797
|
}
|
|
839
798
|
|
|
840
799
|
function buildFileBuffersFromBody(body, isDirectory) {
|
|
841
800
|
if (isDirectory && Array.isArray(body.files)) {
|
|
842
|
-
return body.files.map((file) => ({
|
|
801
|
+
return sortDeliverableFiles(body.files).map((file) => ({
|
|
843
802
|
fileName: file.name || "file",
|
|
844
803
|
content: file.contentText
|
|
845
804
|
? Buffer.from(file.contentText, "utf8")
|
|
@@ -852,11 +811,29 @@ function buildFileBuffersFromBody(body, isDirectory) {
|
|
|
852
811
|
return [{ fileName: body.fileName || "file", content }];
|
|
853
812
|
}
|
|
854
813
|
|
|
855
|
-
function
|
|
814
|
+
function multipartEscape(value) {
|
|
815
|
+
return String(value || "")
|
|
816
|
+
.replace(/\\/g, "\\\\")
|
|
817
|
+
.replace(/"/g, '\\"')
|
|
818
|
+
.replace(/\r/g, "")
|
|
819
|
+
.replace(/\n/g, "");
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function multipartLeafName(value) {
|
|
823
|
+
const normalized = String(value || "file").replace(/\\/g, "/");
|
|
824
|
+
const leaf = path.basename(normalized) || "file";
|
|
825
|
+
return multipartEscape(leaf);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// engineDeliverableUpload posts a deliverable to the kb-engine sync endpoint
|
|
829
|
+
// (multipart: a `payload` JSON field + repeated `files` parts). The engine
|
|
830
|
+
// uploads to the agent root, returns preview/download links immediately, and
|
|
831
|
+
// classifies + moves the file asynchronously. fileBuffers is [{fileName, content:Buffer}].
|
|
832
|
+
function engineDeliverableUpload(fileBuffers, payload) {
|
|
856
833
|
return new Promise((resolve, reject) => {
|
|
857
834
|
let parsed;
|
|
858
835
|
try {
|
|
859
|
-
parsed = new URL(
|
|
836
|
+
parsed = new URL(engineBaseURL() + "/deliverables");
|
|
860
837
|
} catch (err) {
|
|
861
838
|
reject(err);
|
|
862
839
|
return;
|
|
@@ -864,49 +841,41 @@ function kbMultipartUpload(fileBuffers, fields, pathsArray) {
|
|
|
864
841
|
const boundary = "----Deliverables" + Date.now() + Math.random().toString(36).slice(2);
|
|
865
842
|
const parts = [];
|
|
866
843
|
|
|
844
|
+
// payload JSON field
|
|
845
|
+
parts.push(
|
|
846
|
+
Buffer.from(
|
|
847
|
+
"--" + boundary + "\r\n" +
|
|
848
|
+
'Content-Disposition: form-data; name="payload"\r\n\r\n' +
|
|
849
|
+
JSON.stringify(payload) + "\r\n",
|
|
850
|
+
),
|
|
851
|
+
);
|
|
852
|
+
|
|
867
853
|
for (const file of fileBuffers) {
|
|
854
|
+
// Strip CR/LF from the relative path so a newline in a file name cannot
|
|
855
|
+
// inject extra multipart parts; the leaf filename is escaped separately.
|
|
856
|
+
const relativePath = String(file.fileName || "file").replace(/\\/g, "/").replace(/[\r\n]/g, "");
|
|
868
857
|
parts.push(
|
|
869
858
|
Buffer.from(
|
|
870
859
|
"--" + boundary + "\r\n" +
|
|
871
|
-
'Content-Disposition: form-data; name="
|
|
872
|
-
"
|
|
860
|
+
'Content-Disposition: form-data; name="relative_paths"\r\n\r\n' +
|
|
861
|
+
relativePath + "\r\n",
|
|
873
862
|
),
|
|
874
863
|
);
|
|
875
|
-
parts.push(file.content);
|
|
876
|
-
parts.push(Buffer.from("\r\n"));
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
if (Array.isArray(pathsArray) && pathsArray.length > 0) {
|
|
880
|
-
for (const p of pathsArray) {
|
|
881
|
-
parts.push(
|
|
882
|
-
Buffer.from(
|
|
883
|
-
"--" + boundary + "\r\n" +
|
|
884
|
-
'Content-Disposition: form-data; name="paths"\r\n\r\n' +
|
|
885
|
-
(p || "") + "\r\n",
|
|
886
|
-
),
|
|
887
|
-
);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
for (const [key, value] of Object.entries(fields || {})) {
|
|
892
|
-
if (value === undefined || value === null || value === "") {
|
|
893
|
-
continue;
|
|
894
|
-
}
|
|
895
864
|
parts.push(
|
|
896
865
|
Buffer.from(
|
|
897
866
|
"--" + boundary + "\r\n" +
|
|
898
|
-
'Content-Disposition: form-data; name="' +
|
|
899
|
-
|
|
867
|
+
'Content-Disposition: form-data; name="files"; filename="' + multipartLeafName(file.fileName) + '"\r\n' +
|
|
868
|
+
"Content-Type: application/octet-stream\r\n\r\n",
|
|
900
869
|
),
|
|
901
870
|
);
|
|
871
|
+
parts.push(file.content);
|
|
872
|
+
parts.push(Buffer.from("\r\n"));
|
|
902
873
|
}
|
|
903
|
-
|
|
904
874
|
parts.push(Buffer.from("--" + boundary + "--\r\n"));
|
|
905
875
|
const body = Buffer.concat(parts);
|
|
906
876
|
|
|
907
877
|
const isHTTPS = parsed.protocol === "https:";
|
|
908
878
|
const transport = isHTTPS ? https : http;
|
|
909
|
-
const userId = (fields && fields.user_id) || "default";
|
|
910
879
|
const req = transport.request(
|
|
911
880
|
{
|
|
912
881
|
hostname: parsed.hostname,
|
|
@@ -915,11 +884,9 @@ function kbMultipartUpload(fileBuffers, fields, pathsArray) {
|
|
|
915
884
|
method: "POST",
|
|
916
885
|
headers: {
|
|
917
886
|
"Content-Type": "multipart/form-data; boundary=" + boundary,
|
|
918
|
-
"X-User-ID": userId,
|
|
919
|
-
"X-API-Key": kbApiKey(),
|
|
920
887
|
"Content-Length": body.length,
|
|
921
888
|
},
|
|
922
|
-
timeout:
|
|
889
|
+
timeout: 60000,
|
|
923
890
|
},
|
|
924
891
|
(res) => {
|
|
925
892
|
const chunks = [];
|
|
@@ -930,23 +897,25 @@ function kbMultipartUpload(fileBuffers, fields, pathsArray) {
|
|
|
930
897
|
try {
|
|
931
898
|
obj = JSON.parse(text);
|
|
932
899
|
} catch (_err) {
|
|
933
|
-
reject(new Error(`
|
|
900
|
+
reject(new Error(`engine ${res.statusCode}: non-JSON response: ${text.slice(0, 200)}`));
|
|
934
901
|
return;
|
|
935
902
|
}
|
|
936
903
|
if (res.statusCode >= 400) {
|
|
937
|
-
reject(new Error(`
|
|
904
|
+
reject(new Error(`engine ${res.statusCode}: ${obj.err_message || text.slice(0, 200)}`));
|
|
938
905
|
return;
|
|
939
906
|
}
|
|
940
|
-
|
|
941
|
-
|
|
907
|
+
// Unified engine envelope: {code, err_message, data}. code != 0 is a
|
|
908
|
+
// business error.
|
|
909
|
+
if (obj.code !== 0 && obj.code !== undefined) {
|
|
910
|
+
reject(new Error(`engine error: ${obj.err_message || text.slice(0, 200)}`));
|
|
942
911
|
return;
|
|
943
912
|
}
|
|
944
|
-
resolve(obj);
|
|
913
|
+
resolve(obj.data || obj);
|
|
945
914
|
});
|
|
946
915
|
},
|
|
947
916
|
);
|
|
948
917
|
req.on("timeout", () => {
|
|
949
|
-
req.destroy(new Error("
|
|
918
|
+
req.destroy(new Error("engine upload timeout after 60s"));
|
|
950
919
|
});
|
|
951
920
|
req.on("error", reject);
|
|
952
921
|
req.write(body);
|
|
@@ -1263,21 +1232,34 @@ async function buildNativeUploadRequestBody(args) {
|
|
|
1263
1232
|
|
|
1264
1233
|
async function uploadDeliverable(args) {
|
|
1265
1234
|
const { body, isDirectory } = await buildNativeUploadRequestBody(args || {});
|
|
1235
|
+
const conversationType = extractStringField(args || {}, ["conversation_type", "conversationType"]);
|
|
1266
1236
|
const userId = payloadUserId(args) || "default";
|
|
1267
1237
|
const groupId = payloadGroupId(args) || DEFAULT_GROUP_ID;
|
|
1268
|
-
const groupName = extractStringField(args || {}, ["group_name", "groupName"
|
|
1269
|
-
|
|
1238
|
+
const groupName = extractStringField(args || {}, ["group_name", "groupName"]);
|
|
1239
|
+
// lobster_name must be the real context value (or the agent-supplied same
|
|
1240
|
+
// value); no release/resource-id fallback. If still missing, engine rejects it.
|
|
1241
|
+
const lobsterName = payloadLobsterName(args);
|
|
1242
|
+
const lobsterId = payloadLobsterId(args);
|
|
1270
1243
|
const fileBuffers = buildFileBuffersFromBody(body, isDirectory);
|
|
1271
|
-
|
|
1244
|
+
// The engine builds the storage path and classifies — the plugin only supplies
|
|
1245
|
+
// the necessary parameters. content_text (single text file) is a classification
|
|
1246
|
+
// hint so the async classifier can pick a topic without re-reading the file.
|
|
1247
|
+
const payload = {
|
|
1272
1248
|
user_id: userId,
|
|
1249
|
+
conversation_type: conversationType,
|
|
1250
|
+
lobster_name: lobsterName,
|
|
1251
|
+
lobster_id: lobsterId,
|
|
1273
1252
|
group_id: groupId,
|
|
1274
|
-
|
|
1253
|
+
group_name: groupName,
|
|
1254
|
+
deliverable_type: body.type || "",
|
|
1255
|
+
content_text: !isDirectory && isString(body.contentText) ? body.contentText : "",
|
|
1275
1256
|
};
|
|
1276
|
-
const
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1257
|
+
const data = await engineDeliverableUpload(fileBuffers, payload);
|
|
1258
|
+
const previewURL = data.preview_url || data.download_url || "";
|
|
1259
|
+
const downloadURL = data.download_url || previewURL;
|
|
1260
|
+
if (!previewURL && !downloadURL) {
|
|
1261
|
+
throw new Error("engine upload response missing preview_url/download_url");
|
|
1262
|
+
}
|
|
1281
1263
|
const replyMarkdown = buildReplyMarkdown({
|
|
1282
1264
|
previewURL,
|
|
1283
1265
|
downloadURL,
|
|
@@ -1285,9 +1267,11 @@ async function uploadDeliverable(args) {
|
|
|
1285
1267
|
isDirectory,
|
|
1286
1268
|
});
|
|
1287
1269
|
return {
|
|
1288
|
-
document_id:
|
|
1270
|
+
document_id: data.document_id,
|
|
1289
1271
|
download_url: downloadURL,
|
|
1290
1272
|
preview_url: previewURL,
|
|
1273
|
+
category: data.category || "",
|
|
1274
|
+
classify_task_id: data.classify_task_id || "",
|
|
1291
1275
|
reply_markdown: replyMarkdown,
|
|
1292
1276
|
message: replyMarkdown,
|
|
1293
1277
|
};
|
|
@@ -1299,15 +1283,15 @@ function nativeToolResult(result) {
|
|
|
1299
1283
|
};
|
|
1300
1284
|
}
|
|
1301
1285
|
|
|
1302
|
-
function uploadCacheKey(candidate,
|
|
1303
|
-
return `${
|
|
1286
|
+
function uploadCacheKey(candidate, conversationId) {
|
|
1287
|
+
return `${conversationId || ""}:${candidate.path}:${candidate.mtimeMs || 0}:${candidate.size || 0}`;
|
|
1304
1288
|
}
|
|
1305
1289
|
|
|
1306
1290
|
async function uploadCandidate(candidate, body) {
|
|
1307
|
-
const
|
|
1291
|
+
const conversationId = payloadConversationID(body);
|
|
1308
1292
|
const cache = getUploadCache();
|
|
1309
1293
|
pruneTimedMap(cache, AUTO_UPLOAD_TTL_MS);
|
|
1310
|
-
const cacheKey = uploadCacheKey(candidate,
|
|
1294
|
+
const cacheKey = uploadCacheKey(candidate, conversationId);
|
|
1311
1295
|
const cached = cache.get(cacheKey);
|
|
1312
1296
|
if (cached && cached.result) {
|
|
1313
1297
|
return cached.result;
|
|
@@ -1319,12 +1303,19 @@ async function uploadCandidate(candidate, body) {
|
|
|
1319
1303
|
}
|
|
1320
1304
|
|
|
1321
1305
|
const userId = payloadUserId(body) || "default";
|
|
1306
|
+
const conversationType = extractStringField(body || {}, ["conversation_type", "conversationType"]);
|
|
1322
1307
|
const groupId = payloadGroupId(body) || DEFAULT_GROUP_ID;
|
|
1308
|
+
const groupName = extractStringField(body || {}, ["group_name", "groupName"]);
|
|
1309
|
+
// Auto-upload normally sees lobster_name on the outbound message body. If it is
|
|
1310
|
+
// absent, fall back only to lobster_id (a real, stable metadata field), never to
|
|
1311
|
+
// release/resource-derived names.
|
|
1312
|
+
const lobsterId = payloadLobsterId(body);
|
|
1313
|
+
const lobsterName = payloadLobsterName(body) || lobsterId;
|
|
1323
1314
|
const fileName = candidate.fileName || path.basename(candidate.path);
|
|
1324
|
-
const
|
|
1315
|
+
const isDirectory = stat.isDirectory();
|
|
1325
1316
|
|
|
1326
1317
|
let fileBuffers;
|
|
1327
|
-
if (
|
|
1318
|
+
if (isDirectory) {
|
|
1328
1319
|
const files = await collectDirectoryFiles(candidate.path);
|
|
1329
1320
|
if (files.length === 0) {
|
|
1330
1321
|
throw new Error(`directory has no uploadable files: ${candidate.fileName}`);
|
|
@@ -1340,18 +1331,24 @@ async function uploadCandidate(candidate, body) {
|
|
|
1340
1331
|
fileBuffers = [{ fileName, content: await fs.promises.readFile(candidate.path) }];
|
|
1341
1332
|
}
|
|
1342
1333
|
|
|
1343
|
-
const
|
|
1334
|
+
const data = await engineDeliverableUpload(fileBuffers, {
|
|
1344
1335
|
user_id: userId,
|
|
1336
|
+
conversation_type: conversationType,
|
|
1337
|
+
lobster_name: lobsterName,
|
|
1338
|
+
lobster_id: lobsterId,
|
|
1345
1339
|
group_id: groupId,
|
|
1346
|
-
|
|
1340
|
+
group_name: groupName,
|
|
1341
|
+
deliverable_type: deliverableTypeForPath(candidate.path, isDirectory),
|
|
1342
|
+
content_text: "",
|
|
1347
1343
|
});
|
|
1348
|
-
const
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1344
|
+
const previewURL = data.preview_url || data.download_url || "";
|
|
1345
|
+
const downloadURL = data.download_url || previewURL;
|
|
1346
|
+
if (!previewURL && !downloadURL) {
|
|
1347
|
+
throw new Error("engine upload response missing preview_url/download_url");
|
|
1348
|
+
}
|
|
1352
1349
|
const result = {
|
|
1353
1350
|
fileName,
|
|
1354
|
-
document_id:
|
|
1351
|
+
document_id: data.document_id,
|
|
1355
1352
|
previewURL,
|
|
1356
1353
|
downloadURL,
|
|
1357
1354
|
};
|
|
@@ -1781,8 +1778,8 @@ function buildSummaryFromUploadParams(params) {
|
|
|
1781
1778
|
}
|
|
1782
1779
|
|
|
1783
1780
|
function cacheUploadSummary(params) {
|
|
1784
|
-
const
|
|
1785
|
-
if (!
|
|
1781
|
+
const conversationId = payloadConversationID(params || {});
|
|
1782
|
+
if (!conversationId) {
|
|
1786
1783
|
return;
|
|
1787
1784
|
}
|
|
1788
1785
|
const summary = buildSummaryFromUploadParams(params);
|
|
@@ -1790,7 +1787,7 @@ function cacheUploadSummary(params) {
|
|
|
1790
1787
|
return;
|
|
1791
1788
|
}
|
|
1792
1789
|
const cache = getSummaryCache();
|
|
1793
|
-
cache.set(
|
|
1790
|
+
cache.set(conversationId, {
|
|
1794
1791
|
summary,
|
|
1795
1792
|
createdAt: Date.now(),
|
|
1796
1793
|
});
|
|
@@ -2151,7 +2148,7 @@ function shouldSplitPalzPayload(body) {
|
|
|
2151
2148
|
return null;
|
|
2152
2149
|
}
|
|
2153
2150
|
|
|
2154
|
-
const cachedSummary = takeCachedSummary(body
|
|
2151
|
+
const cachedSummary = takeCachedSummary(payloadConversationID(body));
|
|
2155
2152
|
if (cachedSummary && isSummaryTooShort(split.summary)) {
|
|
2156
2153
|
return {
|
|
2157
2154
|
summary: cachedSummary,
|
|
@@ -2408,12 +2405,13 @@ plugin.__test = {
|
|
|
2408
2405
|
isDeliverableLinkLine,
|
|
2409
2406
|
isOSSLikeURL,
|
|
2410
2407
|
isTrustedDeliverableURL,
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2408
|
+
isGroupChat,
|
|
2409
|
+
payloadUserId,
|
|
2410
|
+
payloadGroupId,
|
|
2411
|
+
payloadConversationID,
|
|
2412
|
+
engineDeliverableUpload,
|
|
2413
|
+
enrichDeliverableGroupContext,
|
|
2415
2414
|
loadDeliverableConfig,
|
|
2416
|
-
requireKBReturnedLink,
|
|
2417
2415
|
selectPalzFileURL,
|
|
2418
2416
|
splitDeliverableMessage,
|
|
2419
2417
|
};
|
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.2.
|
|
5
|
+
"version": "1.2.5",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"contracts": {
|
|
8
8
|
"tools": ["deliverables__upload_deliverable"]
|
package/package.json
CHANGED
|
@@ -28,17 +28,21 @@ description: 上传AI生成的文件到交付物系统,返回可分享的预
|
|
|
28
28
|
|
|
29
29
|
| 参数 | 必填 | 取值来源 | 示例 |
|
|
30
30
|
|------|------|---------|------|
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
31
|
+
| `conversation_type` | **是** | 消息元数据 `conversation_type`:单聊 `"direct"`,群聊 `"group"` | `direct` |
|
|
32
|
+
| `conversation_id` | **是** | 消息元数据 `conversation_id`,原样传 | `conv_xxx_main` |
|
|
33
|
+
| `owner_id` | **是** | 消息元数据 `owner_id`,原样传 | `cbb0fab9...` |
|
|
34
|
+
| `sender_id` | **是** | 消息元数据 `sender_id`,原样传 | `cbb0fab9...` |
|
|
35
|
+
| `lobster_name` | **是** | 会话上下文/消息元数据中的 `lobster_name`,作交付物一级目录 | `我的助手` |
|
|
36
|
+
| `lobster_id` | **是** | 消息元数据 `lobster_id`,原样传 | `main` |
|
|
37
|
+
| `group_id` | 群聊**必填** | 群聊取消息元数据 `group_id`;**单聊不传**(自动归个人文件夹) | `grp_xxx` |
|
|
38
|
+
| `group_name` | 群聊**必填** | 群聊取消息元数据中的群聊名称;**单聊不传** | `项目讨论群` |
|
|
34
39
|
| `type` | 是 | 根据内容选择:`article`/`game`/`image`/`video`/`ppt`/`pdf`/`zip`;`link` 仅用于外部 `http(s)` URL | `article` |
|
|
35
40
|
| `file_name` | 是 | 有意义的文件名,单文件必须含扩展名;若用户未指定文档格式,默认用 `.md` | `report-2026.html` |
|
|
36
41
|
| `content_text` | 按需 | 文件的完整文本内容(HTML/Markdown等) | `<html>...</html>` |
|
|
37
42
|
| `file_path` | 按需 | PDF/PPT/图片/zip 等二进制文件的本地路径,推荐使用,避免手工复制 base64 | `output/sample.pdf` |
|
|
38
43
|
|
|
39
|
-
>
|
|
40
|
-
>
|
|
41
|
-
> **⚠ 警告**:`user_id` 必须传入真实值。当 `group_id` 是真实群 ID 但 `user_id` 缺失或为 `"default"` 时,KB 后端会返回 `500: 用户不存在`。
|
|
44
|
+
> **不用自己算 `user_id` / `group_id`**:把上面的原始元数据(`conversation_type`/`conversation_id`/`owner_id`/`sender_id`)照填,插件会自动推导——单聊用 `owner_id` 且归个人文件夹,群聊用 `sender_id` 且归该群。
|
|
45
|
+
> **不要把单聊的 `conversation_id` 当 `group_id` 传**,否则 KB 会因"用户不在群内"上传失败。
|
|
42
46
|
|
|
43
47
|
## 二进制文件上传(强制)
|
|
44
48
|
|